diff --git a/ui/components/component_card.slint b/ui/components/component_card.slint new file mode 100644 index 0000000..c0f602b --- /dev/null +++ b/ui/components/component_card.slint @@ -0,0 +1,42 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { + MaterialText, + OutlinedCard, + MaterialTypography, + MaterialStyleMetrics +} from "../material.slint"; + +export component ComponentCard { + in property title; + + HorizontalLayout { + alignment: center; + + VerticalLayout { + spacing: 4px; + alignment: start; + + MaterialText { + horizontal_alignment: center; + text: root.title; + style: MaterialTypography.title_medium; + } + + OutlinedCard { + min-width: 360px; + + VerticalLayout { + padding_left: 2px; + padding_right: self.padding_left; + padding_top: MaterialStyleMetrics.padding_8; + padding_bottom: self.padding_top; + spacing: MaterialStyleMetrics.spacing_8; + + @children + } + } + } + } +} diff --git a/ui/components/group.slint b/ui/components/group.slint new file mode 100644 index 0000000..d07ed90 --- /dev/null +++ b/ui/components/group.slint @@ -0,0 +1,31 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { + MaterialText, + MaterialPalette, + MaterialTypography +} from "../material.slint"; + +export component Group { + in property title; + + Rectangle { + border_radius: 12px; + background: MaterialPalette.surface_bright; + + VerticalLayout { + alignment: start; + padding: 8px; + spacing: 16px; + + MaterialText { + horizontal_alignment: center; + text: root.title; + style: MaterialTypography.headline_small; + } + + @children + } + } +} diff --git a/ui/components/text_icon_button.slint b/ui/components/text_icon_button.slint new file mode 100644 index 0000000..f51d7b4 --- /dev/null +++ b/ui/components/text_icon_button.slint @@ -0,0 +1,24 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { + IconButton, + MaterialText +} from "../material.slint"; + +export component TextIconButton { + in property icon <=> button.icon; + in property text <=> label.text; + + VerticalLayout { + HorizontalLayout { + alignment: center; + + button := IconButton {} + } + + label := MaterialText { + horizontal_alignment: center; + } + } +} diff --git a/ui/fonts/Roboto-VariableFont.ttf b/ui/fonts/Roboto-VariableFont.ttf new file mode 100644 index 0000000..bba55f6 Binary files /dev/null and b/ui/fonts/Roboto-VariableFont.ttf differ diff --git a/ui/fonts/Roboto-VariableFont.ttf.license b/ui/fonts/Roboto-VariableFont.ttf.license new file mode 100644 index 0000000..7b98a7a --- /dev/null +++ b/ui/fonts/Roboto-VariableFont.ttf.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Google Inc. + +SPDX-License-Identifier: Apache-2.0 diff --git a/ui/icons.slint b/ui/icons.slint new file mode 100644 index 0000000..b7819b2 --- /dev/null +++ b/ui/icons.slint @@ -0,0 +1,73 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +export global FilledIcons { + out property account_box: @image-url("./icons/filled/account_box.svg"); + out property calendar_month: @image-url("./icons/filled/calendar_month.svg"); + out property close: @image-url("./icons/filled/close.svg"); + out property delete: @image-url("./icons/filled/delete.svg"); + out property favorite: @image-url("./icons/filled/favorite.svg"); + out property inbox: @image-url("./icons/filled/inbox.svg"); + out property music_note: @image-url("./icons/filled/music_note.svg"); + out property play_arrow: @image-url("./icons/filled/play_arrow.svg"); + out property today: @image-url("./icons/filled/today.svg"); + out property add: @image-url("./icons/filled/add.svg"); + out property chat_bubble: @image-url("./icons/filled/chat_bubble.svg"); + out property edit: @image-url("./icons/filled/edit.svg"); + out property group: @image-url("./icons/filled/group.svg"); + out property mail: @image-url("./icons/filled/mail.svg"); + out property outbox: @image-url("./icons/filled/outbox.svg"); + out property search: @image-url("./icons/filled/search.svg"); + out property video_chat: @image-url("./icons/filled/video_call.svg"); + out property bookmark: @image-url("./icons/filled/bookmark.svg"); + out property check: @image-url("./icons/filled/check.svg"); + out property explore: @image-url("./icons/filled/explore.svg"); + out property image: @image-url("./icons/filled/image.svg"); + out property more_vert: @image-url("./icons/filled/more_vert.svg"); + out property pets: @image-url("./icons/filled/pets.svg"); + out property settings: @image-url("./icons/filled/settings.svg"); + out property view_week: @image-url("./icons/filled/view_week.svg"); + out property light_mode: @image-url("./icons/filled/light_mode.svg"); + out property dark_mode: @image-url("./icons/filled/dark_mode.svg"); + out property dashboard: @image-url("./icons/filled/dashboard.svg"); + out property pause: @image-url("./icons/filled/pause.svg"); + out property archive: @image-url("./icons/filled/archive.svg"); + out property share: @image-url("./icons/filled/share.svg"); + out property menu: @image-url("./icons/filled/menu.svg"); + out property account_circle: @image-url("./icons/filled/account_circle.svg"); +} + +export global OutlinedIcons { + out property account_box: @image-url("./icons/outlined/account_box.svg"); + out property calendar_month: @image-url("./icons/outlined/calendar_month.svg"); + out property delete: @image-url("./icons/outlined/delete.svg"); + out property favorite: @image-url("./icons/outlined/favorite.svg"); + out property inbox: @image-url("./icons/outlined/inbox.svg"); + out property music_note: @image-url("./icons/outlined/music_note.svg"); + out property play_arrow: @image-url("./icons/outlined/play_arrow.svg"); + out property today: @image-url("./icons/outlined/today.svg"); + out property add: @image-url("./icons/outlined/add.svg"); + out property chat_bubble: @image-url("./icons/outlined/chat_bubble.svg"); + out property edit: @image-url("./icons/outlined/edit.svg"); + out property group: @image-url("./icons/outlined/group.svg"); + out property mail: @image-url("./icons/outlined/mail.svg"); + out property outbox: @image-url("./icons/outlined/outbox.svg"); + out property search: @image-url("./icons/outlined/search.svg"); + out property video_chat: @image-url("./icons/outlined/video_chat.svg"); + out property bookmark: @image-url("./icons/outlined/bookmark.svg"); + out property check: @image-url("./icons/outlined/check.svg"); + out property explore: @image-url("./icons/outlined/explore.svg"); + out property image: @image-url("./icons/outlined/image.svg"); + out property more_vert: @image-url("./icons/outlined/more_vert.svg"); + out property pets: @image-url("./icons/outlined/pets.svg"); + out property settings: @image-url("./icons/outlined/settings.svg"); + out property view_week: @image-url("./icons/outlined/view_week.svg"); + out property light_mode: @image-url("./icons/outlined/light_mode.svg"); + out property dark_mode: @image-url("./icons/outlined/dark_mode.svg"); + out property dashboard: @image-url("./icons/outlined/dashboard.svg"); + out property pause: @image-url("./icons/outlined/pause.svg"); + out property archive: @image-url("./icons/outlined/archive.svg"); + out property share: @image-url("./icons/outlined/share.svg"); + out property schedule: @image-url("./icons/outlined/schedule.svg"); + out property palette: @image-url("./icons/outlined/palette.svg"); + } diff --git a/ui/icons/filled/account_box.svg b/ui/icons/filled/account_box.svg new file mode 100644 index 0000000..e74d64c --- /dev/null +++ b/ui/icons/filled/account_box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/account_circle.svg b/ui/icons/filled/account_circle.svg new file mode 100644 index 0000000..ce42e4f --- /dev/null +++ b/ui/icons/filled/account_circle.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/filled/add.svg b/ui/icons/filled/add.svg new file mode 100644 index 0000000..58f73e9 --- /dev/null +++ b/ui/icons/filled/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/archive.svg b/ui/icons/filled/archive.svg new file mode 100644 index 0000000..024f790 --- /dev/null +++ b/ui/icons/filled/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/bookmark.svg b/ui/icons/filled/bookmark.svg new file mode 100644 index 0000000..1d507a6 --- /dev/null +++ b/ui/icons/filled/bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/calendar_month.svg b/ui/icons/filled/calendar_month.svg new file mode 100644 index 0000000..e8fa87b --- /dev/null +++ b/ui/icons/filled/calendar_month.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/chat_bubble.svg b/ui/icons/filled/chat_bubble.svg new file mode 100644 index 0000000..d447be3 --- /dev/null +++ b/ui/icons/filled/chat_bubble.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/check.svg b/ui/icons/filled/check.svg new file mode 100644 index 0000000..8676ca3 --- /dev/null +++ b/ui/icons/filled/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/close.svg b/ui/icons/filled/close.svg new file mode 100644 index 0000000..373c01a --- /dev/null +++ b/ui/icons/filled/close.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/filled/dark_mode.svg b/ui/icons/filled/dark_mode.svg new file mode 100644 index 0000000..3d141c4 --- /dev/null +++ b/ui/icons/filled/dark_mode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/dashboard.svg b/ui/icons/filled/dashboard.svg new file mode 100644 index 0000000..1682295 --- /dev/null +++ b/ui/icons/filled/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/delete.svg b/ui/icons/filled/delete.svg new file mode 100644 index 0000000..0969a4d --- /dev/null +++ b/ui/icons/filled/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/edit.svg b/ui/icons/filled/edit.svg new file mode 100644 index 0000000..a2841f9 --- /dev/null +++ b/ui/icons/filled/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/explore.svg b/ui/icons/filled/explore.svg new file mode 100644 index 0000000..4243343 --- /dev/null +++ b/ui/icons/filled/explore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/favorite.svg b/ui/icons/filled/favorite.svg new file mode 100644 index 0000000..80f9a05 --- /dev/null +++ b/ui/icons/filled/favorite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/group.svg b/ui/icons/filled/group.svg new file mode 100644 index 0000000..bd9a133 --- /dev/null +++ b/ui/icons/filled/group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/image.svg b/ui/icons/filled/image.svg new file mode 100644 index 0000000..6ccc16a --- /dev/null +++ b/ui/icons/filled/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/inbox.svg b/ui/icons/filled/inbox.svg new file mode 100644 index 0000000..92dbdab --- /dev/null +++ b/ui/icons/filled/inbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/light_mode.svg b/ui/icons/filled/light_mode.svg new file mode 100644 index 0000000..4a29693 --- /dev/null +++ b/ui/icons/filled/light_mode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/mail.svg b/ui/icons/filled/mail.svg new file mode 100644 index 0000000..876dbcd --- /dev/null +++ b/ui/icons/filled/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/menu.svg b/ui/icons/filled/menu.svg new file mode 100644 index 0000000..8c03678 --- /dev/null +++ b/ui/icons/filled/menu.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/filled/more_vert.svg b/ui/icons/filled/more_vert.svg new file mode 100644 index 0000000..f2427f8 --- /dev/null +++ b/ui/icons/filled/more_vert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/music_note.svg b/ui/icons/filled/music_note.svg new file mode 100644 index 0000000..cdc42fc --- /dev/null +++ b/ui/icons/filled/music_note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/outbox.svg b/ui/icons/filled/outbox.svg new file mode 100644 index 0000000..351e92d --- /dev/null +++ b/ui/icons/filled/outbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/pause.svg b/ui/icons/filled/pause.svg new file mode 100644 index 0000000..c20e156 --- /dev/null +++ b/ui/icons/filled/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/pets.svg b/ui/icons/filled/pets.svg new file mode 100644 index 0000000..e4d7d48 --- /dev/null +++ b/ui/icons/filled/pets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/play_arrow.svg b/ui/icons/filled/play_arrow.svg new file mode 100644 index 0000000..c5e1a4c --- /dev/null +++ b/ui/icons/filled/play_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/search.svg b/ui/icons/filled/search.svg new file mode 100644 index 0000000..cd9fd53 --- /dev/null +++ b/ui/icons/filled/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/settings.svg b/ui/icons/filled/settings.svg new file mode 100644 index 0000000..f255a58 --- /dev/null +++ b/ui/icons/filled/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/share.svg b/ui/icons/filled/share.svg new file mode 100644 index 0000000..ed3e530 --- /dev/null +++ b/ui/icons/filled/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/today.svg b/ui/icons/filled/today.svg new file mode 100644 index 0000000..43abd72 --- /dev/null +++ b/ui/icons/filled/today.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/video_call.svg b/ui/icons/filled/video_call.svg new file mode 100644 index 0000000..e1a21e1 --- /dev/null +++ b/ui/icons/filled/video_call.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/filled/view_week.svg b/ui/icons/filled/view_week.svg new file mode 100644 index 0000000..d26bb06 --- /dev/null +++ b/ui/icons/filled/view_week.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/account_box.svg b/ui/icons/outlined/account_box.svg new file mode 100644 index 0000000..3875711 --- /dev/null +++ b/ui/icons/outlined/account_box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/add.svg b/ui/icons/outlined/add.svg new file mode 100644 index 0000000..58f73e9 --- /dev/null +++ b/ui/icons/outlined/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/archive.svg b/ui/icons/outlined/archive.svg new file mode 100644 index 0000000..7318216 --- /dev/null +++ b/ui/icons/outlined/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/bookmark.svg b/ui/icons/outlined/bookmark.svg new file mode 100644 index 0000000..812bffe --- /dev/null +++ b/ui/icons/outlined/bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/calendar_month.svg b/ui/icons/outlined/calendar_month.svg new file mode 100644 index 0000000..c35f75e --- /dev/null +++ b/ui/icons/outlined/calendar_month.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/chat_bubble.svg b/ui/icons/outlined/chat_bubble.svg new file mode 100644 index 0000000..d447be3 --- /dev/null +++ b/ui/icons/outlined/chat_bubble.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/check.svg b/ui/icons/outlined/check.svg new file mode 100644 index 0000000..5e3f01e --- /dev/null +++ b/ui/icons/outlined/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/dark_mode.svg b/ui/icons/outlined/dark_mode.svg new file mode 100644 index 0000000..c38c55e --- /dev/null +++ b/ui/icons/outlined/dark_mode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/dashboard.svg b/ui/icons/outlined/dashboard.svg new file mode 100644 index 0000000..01ef592 --- /dev/null +++ b/ui/icons/outlined/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/delete.svg b/ui/icons/outlined/delete.svg new file mode 100644 index 0000000..14508a2 --- /dev/null +++ b/ui/icons/outlined/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/edit.svg b/ui/icons/outlined/edit.svg new file mode 100644 index 0000000..01f34c7 --- /dev/null +++ b/ui/icons/outlined/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/explore.svg b/ui/icons/outlined/explore.svg new file mode 100644 index 0000000..b62e5ed --- /dev/null +++ b/ui/icons/outlined/explore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/favorite.svg b/ui/icons/outlined/favorite.svg new file mode 100644 index 0000000..80f9a05 --- /dev/null +++ b/ui/icons/outlined/favorite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/group.svg b/ui/icons/outlined/group.svg new file mode 100644 index 0000000..7fa6132 --- /dev/null +++ b/ui/icons/outlined/group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/image.svg b/ui/icons/outlined/image.svg new file mode 100644 index 0000000..5400d38 --- /dev/null +++ b/ui/icons/outlined/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/inbox.svg b/ui/icons/outlined/inbox.svg new file mode 100644 index 0000000..fc55d23 --- /dev/null +++ b/ui/icons/outlined/inbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/light_mode.svg b/ui/icons/outlined/light_mode.svg new file mode 100644 index 0000000..d66b3ab --- /dev/null +++ b/ui/icons/outlined/light_mode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/mail.svg b/ui/icons/outlined/mail.svg new file mode 100644 index 0000000..f3809a5 --- /dev/null +++ b/ui/icons/outlined/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/more_vert.svg b/ui/icons/outlined/more_vert.svg new file mode 100644 index 0000000..f2427f8 --- /dev/null +++ b/ui/icons/outlined/more_vert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/music_note.svg b/ui/icons/outlined/music_note.svg new file mode 100644 index 0000000..e7be7bf --- /dev/null +++ b/ui/icons/outlined/music_note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/outbox.svg b/ui/icons/outlined/outbox.svg new file mode 100644 index 0000000..ae8bda3 --- /dev/null +++ b/ui/icons/outlined/outbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/palette.svg b/ui/icons/outlined/palette.svg new file mode 100644 index 0000000..6475eb3 --- /dev/null +++ b/ui/icons/outlined/palette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/pause.svg b/ui/icons/outlined/pause.svg new file mode 100644 index 0000000..c20e156 --- /dev/null +++ b/ui/icons/outlined/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/pets.svg b/ui/icons/outlined/pets.svg new file mode 100644 index 0000000..e4d7d48 --- /dev/null +++ b/ui/icons/outlined/pets.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/play_arrow.svg b/ui/icons/outlined/play_arrow.svg new file mode 100644 index 0000000..842d32f --- /dev/null +++ b/ui/icons/outlined/play_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/schedule.svg b/ui/icons/outlined/schedule.svg new file mode 100644 index 0000000..6ed61e0 --- /dev/null +++ b/ui/icons/outlined/schedule.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/outlined/search.svg b/ui/icons/outlined/search.svg new file mode 100644 index 0000000..cd9fd53 --- /dev/null +++ b/ui/icons/outlined/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/settings.svg b/ui/icons/outlined/settings.svg new file mode 100644 index 0000000..b36758c --- /dev/null +++ b/ui/icons/outlined/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/share.svg b/ui/icons/outlined/share.svg new file mode 100644 index 0000000..b3295db --- /dev/null +++ b/ui/icons/outlined/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/today.svg b/ui/icons/outlined/today.svg new file mode 100644 index 0000000..b3faffc --- /dev/null +++ b/ui/icons/outlined/today.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/video_chat.svg b/ui/icons/outlined/video_chat.svg new file mode 100644 index 0000000..798cd1e --- /dev/null +++ b/ui/icons/outlined/video_chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/outlined/view_week.svg b/ui/icons/outlined/view_week.svg new file mode 100644 index 0000000..e7a9e7d --- /dev/null +++ b/ui/icons/outlined/view_week.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/images/blue-hint.png b/ui/images/blue-hint.png deleted file mode 100644 index 442e17a..0000000 Binary files a/ui/images/blue-hint.png and /dev/null differ diff --git a/ui/images/dial-frame.png b/ui/images/dial-frame.png deleted file mode 100644 index a765a1c..0000000 Binary files a/ui/images/dial-frame.png and /dev/null differ diff --git a/ui/images/dial-trim.png b/ui/images/dial-trim.png deleted file mode 100644 index 7548d06..0000000 Binary files a/ui/images/dial-trim.png and /dev/null differ diff --git a/ui/images/indicator.png b/ui/images/indicator.png deleted file mode 100644 index aea782f..0000000 Binary files a/ui/images/indicator.png and /dev/null differ diff --git a/ui/images/light-hole.png b/ui/images/light-hole.png deleted file mode 100644 index 0a2bb48..0000000 Binary files a/ui/images/light-hole.png and /dev/null differ diff --git a/ui/images/light.png b/ui/images/light.png deleted file mode 100644 index 71ba5b9..0000000 Binary files a/ui/images/light.png and /dev/null differ diff --git a/ui/images/metal-dial.png b/ui/images/metal-dial.png deleted file mode 100644 index c0095ff..0000000 Binary files a/ui/images/metal-dial.png and /dev/null differ diff --git a/ui/images/metal-lights.png b/ui/images/metal-lights.png deleted file mode 100644 index 2208500..0000000 Binary files a/ui/images/metal-lights.png and /dev/null differ diff --git a/ui/images/slint-logo-square-dark.png b/ui/images/slint-logo-square-dark.png deleted file mode 100644 index c3eb018..0000000 Binary files a/ui/images/slint-logo-square-dark.png and /dev/null differ diff --git a/ui/main.slint b/ui/main.slint index 41e687d..6ab3b05 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -2,6 +2,12 @@ // SPDX-License-Identifier: MIT import { LineEdit } from "std-widgets.slint"; +import { ComponentCard } from "./components/component_card.slint"; + +import { Horizontal, Vertical, Switch } from "./material.slint"; + +import "./fonts/Roboto-VariableFont.ttf"; +import { FilledIcons, OutlinedIcons } from "./icons.slint"; // The window the user interacts with. Edits to the inputs propagate to // the SystemTrayIcon instance via the host language's callbacks. @@ -9,11 +15,12 @@ export component MainWindow inherits Window { title: "Dashboard"; preferred-width: 1024px; preferred-height: 600px; + default-font-family: "Roboto"; in-out property current-page: 0; in property text: "Hello World"; in property next-event-label: "NEXT EVENT"; - in property next-event-title: "LOADING CALENDAR LORUM IPSUM DOLOR SIT AMET"; + in property next-event-title: "LOADING CALENDAR"; in property next-event-time: "6:00 pm"; callback user-activity(); @@ -75,4 +82,42 @@ export component MainWindow inherits Window { horizontal-alignment: right; vertical-alignment: center; } + + Rectangle { + x: 38px; + y: 32px + 36px + 5px; + width: (root.width / 2) - 38px; + height: root.height - 32px + 36px + 5px; + + ComponentCard { + title: "Switches"; + + Vertical { + Horizontal { + alignment: space_around; + + Switch {} + + Switch { + off_icon: FilledIcons.close; + on_icon: FilledIcons.check; + } + } + + Horizontal { + alignment: space_around; + + Switch { + enabled: false; + } + + Switch { + enabled: false; + on_icon: FilledIcons.check; + off_icon: FilledIcons.close; + } + } + } + } + } } diff --git a/ui/material.slint b/ui/material.slint new file mode 100644 index 0000000..4b5316f --- /dev/null +++ b/ui/material.slint @@ -0,0 +1,66 @@ +// Copyright © SixtyFPS GmbH +// 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"; diff --git a/ui/themes/material_green_theme.json b/ui/themes/material_green_theme.json new file mode 100644 index 0000000..7cd677f --- /dev/null +++ b/ui/themes/material_green_theme.json @@ -0,0 +1,418 @@ +{ + "description": "TYPE: CUSTOM\nMaterial Theme Builder export 2025-08-29 10:20:31", + "seed": "#63A002", + "coreColors": { + "primary": "#63A002" + }, + "extendedColors": [], + "schemes": { + "light": { + "primary": "#4C662B", + "surfaceTint": "#4C662B", + "onPrimary": "#FFFFFF", + "primaryContainer": "#CDEDA3", + "onPrimaryContainer": "#354E16", + "secondary": "#586249", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#DCE7C8", + "onSecondaryContainer": "#404A33", + "tertiary": "#386663", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#BCECE7", + "onTertiaryContainer": "#1F4E4B", + "error": "#BA1A1A", + "onError": "#FFFFFF", + "errorContainer": "#FFDAD6", + "onErrorContainer": "#93000A", + "background": "#F9FAEF", + "onBackground": "#1A1C16", + "surface": "#F9FAEF", + "onSurface": "#1A1C16", + "surfaceVariant": "#E1E4D5", + "onSurfaceVariant": "#44483D", + "outline": "#75796C", + "outlineVariant": "#C5C8BA", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2F312A", + "inverseOnSurface": "#F1F2E6", + "inversePrimary": "#B1D18A", + "primaryFixed": "#CDEDA3", + "onPrimaryFixed": "#102000", + "primaryFixedDim": "#B1D18A", + "onPrimaryFixedVariant": "#354E16", + "secondaryFixed": "#DCE7C8", + "onSecondaryFixed": "#151E0B", + "secondaryFixedDim": "#BFCBAD", + "onSecondaryFixedVariant": "#404A33", + "tertiaryFixed": "#BCECE7", + "onTertiaryFixed": "#00201E", + "tertiaryFixedDim": "#A0D0CB", + "onTertiaryFixedVariant": "#1F4E4B", + "surfaceDim": "#DADBD0", + "surfaceBright": "#F9FAEF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F3F4E9", + "surfaceContainer": "#EEEFE3", + "surfaceContainerHigh": "#E8E9DE", + "surfaceContainerHighest": "#E2E3D8" + }, + "light-medium-contrast": { + "primary": "#253D05", + "surfaceTint": "#4C662B", + "onPrimary": "#FFFFFF", + "primaryContainer": "#5A7539", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#303924", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#667157", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#083D3A", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#477572", + "onTertiaryContainer": "#FFFFFF", + "error": "#740006", + "onError": "#FFFFFF", + "errorContainer": "#CF2C27", + "onErrorContainer": "#FFFFFF", + "background": "#F9FAEF", + "onBackground": "#1A1C16", + "surface": "#F9FAEF", + "onSurface": "#0F120C", + "surfaceVariant": "#E1E4D5", + "onSurfaceVariant": "#34382D", + "outline": "#505449", + "outlineVariant": "#6B6F62", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2F312A", + "inverseOnSurface": "#F1F2E6", + "inversePrimary": "#B1D18A", + "primaryFixed": "#5A7539", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#425C23", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#667157", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#4E5840", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#477572", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#2E5C59", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#C6C7BD", + "surfaceBright": "#F9FAEF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F3F4E9", + "surfaceContainer": "#E8E9DE", + "surfaceContainerHigh": "#DCDED3", + "surfaceContainerHighest": "#D1D3C8" + }, + "light-high-contrast": { + "primary": "#1C3200", + "surfaceTint": "#4C662B", + "onPrimary": "#FFFFFF", + "primaryContainer": "#375018", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#262F1A", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#434C35", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#003230", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#21504E", + "onTertiaryContainer": "#FFFFFF", + "error": "#600004", + "onError": "#FFFFFF", + "errorContainer": "#98000A", + "onErrorContainer": "#FFFFFF", + "background": "#F9FAEF", + "onBackground": "#1A1C16", + "surface": "#F9FAEF", + "onSurface": "#000000", + "surfaceVariant": "#E1E4D5", + "onSurfaceVariant": "#000000", + "outline": "#2A2D24", + "outlineVariant": "#474B40", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2F312A", + "inverseOnSurface": "#FFFFFF", + "inversePrimary": "#B1D18A", + "primaryFixed": "#375018", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#213903", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#434C35", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#2C3620", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#21504E", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#033937", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#B8BAAF", + "surfaceBright": "#F9FAEF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F1F2E6", + "surfaceContainer": "#E2E3D8", + "surfaceContainerHigh": "#D4D5CA", + "surfaceContainerHighest": "#C6C7BD" + }, + "dark": { + "primary": "#B1D18A", + "surfaceTint": "#B1D18A", + "onPrimary": "#1F3701", + "primaryContainer": "#354E16", + "onPrimaryContainer": "#CDEDA3", + "secondary": "#BFCBAD", + "onSecondary": "#2A331E", + "secondaryContainer": "#404A33", + "onSecondaryContainer": "#DCE7C8", + "tertiary": "#A0D0CB", + "onTertiary": "#003735", + "tertiaryContainer": "#1F4E4B", + "onTertiaryContainer": "#BCECE7", + "error": "#FFB4AB", + "onError": "#690005", + "errorContainer": "#93000A", + "onErrorContainer": "#FFDAD6", + "background": "#12140E", + "onBackground": "#E2E3D8", + "surface": "#12140E", + "onSurface": "#E2E3D8", + "surfaceVariant": "#44483D", + "onSurfaceVariant": "#C5C8BA", + "outline": "#8F9285", + "outlineVariant": "#44483D", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E2E3D8", + "inverseOnSurface": "#2F312A", + "inversePrimary": "#4C662B", + "primaryFixed": "#CDEDA3", + "onPrimaryFixed": "#102000", + "primaryFixedDim": "#B1D18A", + "onPrimaryFixedVariant": "#354E16", + "secondaryFixed": "#DCE7C8", + "onSecondaryFixed": "#151E0B", + "secondaryFixedDim": "#BFCBAD", + "onSecondaryFixedVariant": "#404A33", + "tertiaryFixed": "#BCECE7", + "onTertiaryFixed": "#00201E", + "tertiaryFixedDim": "#A0D0CB", + "onTertiaryFixedVariant": "#1F4E4B", + "surfaceDim": "#12140E", + "surfaceBright": "#383A32", + "surfaceContainerLowest": "#0C0F09", + "surfaceContainerLow": "#1A1C16", + "surfaceContainer": "#1E201A", + "surfaceContainerHigh": "#282B24", + "surfaceContainerHighest": "#33362E" + }, + "dark-medium-contrast": { + "primary": "#C7E79E", + "surfaceTint": "#B1D18A", + "onPrimary": "#172B00", + "primaryContainer": "#7D9A59", + "onPrimaryContainer": "#000000", + "secondary": "#D5E1C2", + "onSecondary": "#1F2814", + "secondaryContainer": "#8A9579", + "onSecondaryContainer": "#000000", + "tertiary": "#B5E6E1", + "onTertiary": "#002B29", + "tertiaryContainer": "#6B9995", + "onTertiaryContainer": "#000000", + "error": "#FFD2CC", + "onError": "#540003", + "errorContainer": "#FF5449", + "onErrorContainer": "#000000", + "background": "#12140E", + "onBackground": "#E2E3D8", + "surface": "#12140E", + "onSurface": "#FFFFFF", + "surfaceVariant": "#44483D", + "onSurfaceVariant": "#DBDECF", + "outline": "#B0B3A6", + "outlineVariant": "#8E9285", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E2E3D8", + "inverseOnSurface": "#282B24", + "inversePrimary": "#364F17", + "primaryFixed": "#CDEDA3", + "onPrimaryFixed": "#081400", + "primaryFixedDim": "#B1D18A", + "onPrimaryFixedVariant": "#253D05", + "secondaryFixed": "#DCE7C8", + "onSecondaryFixed": "#0B1403", + "secondaryFixedDim": "#BFCBAD", + "onSecondaryFixedVariant": "#303924", + "tertiaryFixed": "#BCECE7", + "onTertiaryFixed": "#001413", + "tertiaryFixedDim": "#A0D0CB", + "onTertiaryFixedVariant": "#083D3A", + "surfaceDim": "#12140E", + "surfaceBright": "#43453D", + "surfaceContainerLowest": "#060804", + "surfaceContainerLow": "#1C1E18", + "surfaceContainer": "#262922", + "surfaceContainerHigh": "#31342C", + "surfaceContainerHighest": "#3C3F37" + }, + "dark-high-contrast": { + "primary": "#DAFBB0", + "surfaceTint": "#B1D18A", + "onPrimary": "#000000", + "primaryContainer": "#ADCD86", + "onPrimaryContainer": "#050E00", + "secondary": "#E9F4D5", + "onSecondary": "#000000", + "secondaryContainer": "#BCC7A9", + "onSecondaryContainer": "#060D01", + "tertiary": "#C9F9F5", + "onTertiary": "#000000", + "tertiaryContainer": "#9CCCC7", + "onTertiaryContainer": "#000E0D", + "error": "#FFECE9", + "onError": "#000000", + "errorContainer": "#FFAEA4", + "onErrorContainer": "#220001", + "background": "#12140E", + "onBackground": "#E2E3D8", + "surface": "#12140E", + "onSurface": "#FFFFFF", + "surfaceVariant": "#44483D", + "onSurfaceVariant": "#FFFFFF", + "outline": "#EEF2E2", + "outlineVariant": "#C1C4B6", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E2E3D8", + "inverseOnSurface": "#000000", + "inversePrimary": "#364F17", + "primaryFixed": "#CDEDA3", + "onPrimaryFixed": "#000000", + "primaryFixedDim": "#B1D18A", + "onPrimaryFixedVariant": "#081400", + "secondaryFixed": "#DCE7C8", + "onSecondaryFixed": "#000000", + "secondaryFixedDim": "#BFCBAD", + "onSecondaryFixedVariant": "#0B1403", + "tertiaryFixed": "#BCECE7", + "onTertiaryFixed": "#000000", + "tertiaryFixedDim": "#A0D0CB", + "onTertiaryFixedVariant": "#001413", + "surfaceDim": "#12140E", + "surfaceBright": "#4F5149", + "surfaceContainerLowest": "#000000", + "surfaceContainerLow": "#1E201A", + "surfaceContainer": "#2F312A", + "surfaceContainerHigh": "#3A3C35", + "surfaceContainerHighest": "#454840" + } + }, + "palettes": { + "primary": { + "0": "#000000", + "5": "#081400", + "10": "#102000", + "15": "#172B00", + "20": "#1F3700", + "25": "#264300", + "30": "#2F4F00", + "35": "#375C00", + "40": "#3F6900", + "50": "#518500", + "60": "#64A104", + "70": "#7DBD2A", + "80": "#97D945", + "90": "#B2F65F", + "95": "#D2FF9B", + "98": "#F0FFD7", + "99": "#F9FFE9", + "100": "#FFFFFF" + }, + "secondary": { + "0": "#000000", + "5": "#081400", + "10": "#121F04", + "15": "#1C2A0B", + "20": "#263515", + "25": "#31401F", + "30": "#3C4C2A", + "35": "#485734", + "40": "#54643F", + "50": "#6C7D56", + "60": "#85976E", + "70": "#A0B187", + "80": "#BBCDA1", + "90": "#D7E9BB", + "95": "#E5F7C9", + "98": "#F0FFD7", + "99": "#F9FFE9", + "100": "#FFFFFF" + }, + "tertiary": { + "0": "#000000", + "5": "#001413", + "10": "#00201E", + "15": "#002B29", + "20": "#003735", + "25": "#004340", + "30": "#00504C", + "35": "#005D59", + "40": "#046A66", + "50": "#30837F", + "60": "#4D9D98", + "70": "#69B8B3", + "80": "#85D4CF", + "90": "#A1F1EB", + "95": "#B0FFF9", + "98": "#E3FFFC", + "99": "#F2FFFD", + "100": "#FFFFFF" + }, + "neutral": { + "0": "#000000", + "5": "#10110D", + "10": "#1B1C18", + "15": "#252622", + "20": "#30312C", + "25": "#3B3C37", + "30": "#464742", + "35": "#52534D", + "40": "#5E5F59", + "50": "#777771", + "60": "#91918B", + "70": "#ABACA5", + "80": "#C7C7C0", + "90": "#E3E3DB", + "95": "#F2F1E9", + "98": "#FAFAF2", + "99": "#FDFCF5", + "100": "#FFFFFF" + }, + "neutral-variant": { + "0": "#000000", + "5": "#0E120A", + "10": "#191D14", + "15": "#23271D", + "20": "#2E3228", + "25": "#393D32", + "30": "#44483D", + "35": "#505449", + "40": "#5C6054", + "50": "#75796C", + "60": "#8F9285", + "70": "#A9AD9F", + "80": "#C5C8BA", + "90": "#E1E4D5", + "95": "#EFF2E3", + "98": "#F8FBEB", + "99": "#FBFEEE", + "100": "#FFFFFF" + } + } +} \ No newline at end of file diff --git a/ui/themes/material_purple_theme.json b/ui/themes/material_purple_theme.json new file mode 100644 index 0000000..4fa176b --- /dev/null +++ b/ui/themes/material_purple_theme.json @@ -0,0 +1,429 @@ +{ + "description": "TYPE: CUSTOM\nMaterial Theme Builder export 2025-08-28 04:01:30", + "seed": "#6750A4", + "coreColors": { + "primary": "#6750A4", + "secondary": "#958DA4", + "tertiary": "#B58392", + "neutral": "#938F94", + "neutralVariant": "#948F99" + }, + "extendedColors": [ + { + "name": "Custom Color 1", + "color": "#166EA6", + "description": "", + "harmonized": false + } + ], + "schemes": { + "light": { + "primary": "#65558F", + "surfaceTint": "#65558F", + "onPrimary": "#FFFFFF", + "primaryContainer": "#E9DDFF", + "onPrimaryContainer": "#4D3D75", + "secondary": "#65558F", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#E9DDFF", + "onSecondaryContainer": "#4D3D75", + "tertiary": "#8B4A61", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#FFD9E3", + "onTertiaryContainer": "#6F3349", + "error": "#BA1A1A", + "onError": "#FFFFFF", + "errorContainer": "#FFDAD6", + "onErrorContainer": "#93000A", + "background": "#FDF7FF", + "onBackground": "#1D1B20", + "surface": "#FEF7FF", + "onSurface": "#1D1B20", + "surfaceVariant": "#E7E0EB", + "onSurfaceVariant": "#49454E", + "outline": "#7A757F", + "outlineVariant": "#CAC4CF", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#322F35", + "inverseOnSurface": "#F5EFF7", + "inversePrimary": "#CFBDFE", + "primaryFixed": "#E9DDFF", + "onPrimaryFixed": "#201047", + "primaryFixedDim": "#CFBDFE", + "onPrimaryFixedVariant": "#4D3D75", + "secondaryFixed": "#E9DDFF", + "onSecondaryFixed": "#210F47", + "secondaryFixedDim": "#D0BCFE", + "onSecondaryFixedVariant": "#4D3D75", + "tertiaryFixed": "#FFD9E3", + "onTertiaryFixed": "#3A071E", + "tertiaryFixedDim": "#FFB0C9", + "onTertiaryFixedVariant": "#6F3349", + "surfaceDim": "#DED8E0", + "surfaceBright": "#FEF7FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F8F1FA", + "surfaceContainer": "#F2ECF4", + "surfaceContainerHigh": "#ECE6EE", + "surfaceContainerHighest": "#E7E0E8" + }, + "light-medium-contrast": { + "primary": "#3C2D63", + "surfaceTint": "#65558F", + "onPrimary": "#FFFFFF", + "primaryContainer": "#74649F", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#3C2C63", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#74649E", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#5B2238", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#9C5870", + "onTertiaryContainer": "#FFFFFF", + "error": "#740006", + "onError": "#FFFFFF", + "errorContainer": "#CF2C27", + "onErrorContainer": "#FFFFFF", + "background": "#FDF7FF", + "onBackground": "#1D1B20", + "surface": "#FEF7FF", + "onSurface": "#121016", + "surfaceVariant": "#E7E0EB", + "onSurfaceVariant": "#38353D", + "outline": "#55515A", + "outlineVariant": "#706B75", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#322F35", + "inverseOnSurface": "#F5EFF7", + "inversePrimary": "#CFBDFE", + "primaryFixed": "#74649F", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#5B4C84", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#74649E", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#5B4B84", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#9C5870", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#804057", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#CAC5CC", + "surfaceBright": "#FEF7FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F8F1FA", + "surfaceContainer": "#ECE6EE", + "surfaceContainerHigh": "#E1DBE3", + "surfaceContainerHighest": "#D5D0D8" + }, + "light-high-contrast": { + "primary": "#312259", + "surfaceTint": "#65558F", + "onPrimary": "#FFFFFF", + "primaryContainer": "#4F4078", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#322258", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#504078", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#4F182E", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#72354C", + "onTertiaryContainer": "#FFFFFF", + "error": "#600004", + "onError": "#FFFFFF", + "errorContainer": "#98000A", + "onErrorContainer": "#FFFFFF", + "background": "#FDF7FF", + "onBackground": "#1D1B20", + "surface": "#FEF7FF", + "onSurface": "#000000", + "surfaceVariant": "#E7E0EB", + "onSurfaceVariant": "#000000", + "outline": "#2E2B33", + "outlineVariant": "#4B4851", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#322F35", + "inverseOnSurface": "#FFFFFF", + "inversePrimary": "#CFBDFE", + "primaryFixed": "#4F4078", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#382960", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#504078", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#38295F", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#72354C", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#571F35", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#BCB7BF", + "surfaceBright": "#FEF7FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F5EFF7", + "surfaceContainer": "#E7E0E8", + "surfaceContainerHigh": "#D8D2DA", + "surfaceContainerHighest": "#CAC5CC" + }, + "dark": { + "primary": "#CFBDFE", + "surfaceTint": "#CFBDFE", + "onPrimary": "#36275D", + "primaryContainer": "#4D3D75", + "onPrimaryContainer": "#E9DDFF", + "secondary": "#D0BCFE", + "onSecondary": "#36265D", + "secondaryContainer": "#4D3D75", + "onSecondaryContainer": "#E9DDFF", + "tertiary": "#FFB0C9", + "onTertiary": "#541D33", + "tertiaryContainer": "#6F3349", + "onTertiaryContainer": "#FFD9E3", + "error": "#FFB4AB", + "onError": "#690005", + "errorContainer": "#93000A", + "onErrorContainer": "#FFDAD6", + "background": "#141218", + "onBackground": "#E6E0E9", + "surface": "#141218", + "onSurface": "#E7E0E8", + "surfaceVariant": "#49454E", + "onSurfaceVariant": "#CAC4CF", + "outline": "#948F99", + "outlineVariant": "#49454E", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E7E0E8", + "inverseOnSurface": "#322F35", + "inversePrimary": "#65558F", + "primaryFixed": "#E9DDFF", + "onPrimaryFixed": "#201047", + "primaryFixedDim": "#CFBDFE", + "onPrimaryFixedVariant": "#4D3D75", + "secondaryFixed": "#E9DDFF", + "onSecondaryFixed": "#210F47", + "secondaryFixedDim": "#D0BCFE", + "onSecondaryFixedVariant": "#4D3D75", + "tertiaryFixed": "#FFD9E3", + "onTertiaryFixed": "#3A071E", + "tertiaryFixedDim": "#FFB0C9", + "onTertiaryFixedVariant": "#6F3349", + "surfaceDim": "#141218", + "surfaceBright": "#3B383E", + "surfaceContainerLowest": "#0F0D13", + "surfaceContainerLow": "#1D1B20", + "surfaceContainer": "#211F24", + "surfaceContainerHigh": "#2B292F", + "surfaceContainerHighest": "#36343A" + }, + "dark-medium-contrast": { + "primary": "#E3D6FF", + "surfaceTint": "#CFBDFE", + "onPrimary": "#2B1B52", + "primaryContainer": "#9887C5", + "onPrimaryContainer": "#000000", + "secondary": "#E3D6FF", + "onSecondary": "#2B1B51", + "secondaryContainer": "#9987C5", + "onSecondaryContainer": "#000000", + "tertiary": "#FFD0DD", + "onTertiary": "#471228", + "tertiaryContainer": "#C57B93", + "onTertiaryContainer": "#000000", + "error": "#FFD2CC", + "onError": "#540003", + "errorContainer": "#FF5449", + "onErrorContainer": "#000000", + "background": "#141218", + "onBackground": "#E6E0E9", + "surface": "#141218", + "onSurface": "#FFFFFF", + "surfaceVariant": "#49454E", + "onSurfaceVariant": "#E0DAE5", + "outline": "#B5B0BB", + "outlineVariant": "#938E99", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E7E0E8", + "inverseOnSurface": "#2B292F", + "inversePrimary": "#4E3F77", + "primaryFixed": "#E9DDFF", + "onPrimaryFixed": "#16033D", + "primaryFixedDim": "#CFBDFE", + "onPrimaryFixedVariant": "#3C2D63", + "secondaryFixed": "#E9DDFF", + "onSecondaryFixed": "#16033D", + "secondaryFixedDim": "#D0BCFE", + "onSecondaryFixedVariant": "#3C2C63", + "tertiaryFixed": "#FFD9E3", + "onTertiaryFixed": "#2B0013", + "tertiaryFixedDim": "#FFB0C9", + "onTertiaryFixedVariant": "#5B2238", + "surfaceDim": "#141218", + "surfaceBright": "#46434A", + "surfaceContainerLowest": "#08070B", + "surfaceContainerLow": "#1F1D22", + "surfaceContainer": "#29272D", + "surfaceContainerHigh": "#343138", + "surfaceContainerHighest": "#3F3C43" + }, + "dark-high-contrast": { + "primary": "#F5EDFF", + "surfaceTint": "#CFBDFE", + "onPrimary": "#000000", + "primaryContainer": "#CBB9FA", + "onPrimaryContainer": "#0F0033", + "secondary": "#F5EDFF", + "onSecondary": "#000000", + "secondaryContainer": "#CCB9FA", + "onSecondaryContainer": "#100032", + "tertiary": "#FFEBEF", + "onTertiary": "#000000", + "tertiaryContainer": "#FEABC5", + "onTertiaryContainer": "#20000D", + "error": "#FFECE9", + "onError": "#000000", + "errorContainer": "#FFAEA4", + "onErrorContainer": "#220001", + "background": "#141218", + "onBackground": "#E6E0E9", + "surface": "#141218", + "onSurface": "#FFFFFF", + "surfaceVariant": "#49454E", + "onSurfaceVariant": "#FFFFFF", + "outline": "#F4EDF9", + "outlineVariant": "#C6C0CB", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E7E0E8", + "inverseOnSurface": "#000000", + "inversePrimary": "#4E3F77", + "primaryFixed": "#E9DDFF", + "onPrimaryFixed": "#000000", + "primaryFixedDim": "#CFBDFE", + "onPrimaryFixedVariant": "#16033D", + "secondaryFixed": "#E9DDFF", + "onSecondaryFixed": "#000000", + "secondaryFixedDim": "#D0BCFE", + "onSecondaryFixedVariant": "#16033D", + "tertiaryFixed": "#FFD9E3", + "onTertiaryFixed": "#000000", + "tertiaryFixedDim": "#FFB0C9", + "onTertiaryFixedVariant": "#2B0013", + "surfaceDim": "#141218", + "surfaceBright": "#524F55", + "surfaceContainerLowest": "#000000", + "surfaceContainerLow": "#211F24", + "surfaceContainer": "#322F35", + "surfaceContainerHigh": "#3D3A40", + "surfaceContainerHighest": "#49454C" + } + }, + "palettes": { + "primary": { + "0": "#000000", + "5": "#160041", + "10": "#22005D", + "15": "#2D1067", + "20": "#381E72", + "25": "#432B7E", + "30": "#4F378A", + "35": "#5B4397", + "40": "#6750A4", + "50": "#8069BF", + "60": "#9A83DB", + "70": "#B69DF7", + "80": "#CFBCFF", + "90": "#E9DDFF", + "95": "#F6EEFF", + "98": "#FDF7FF", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "secondary": { + "0": "#000000", + "5": "#130E1F", + "10": "#1E192B", + "15": "#282335", + "20": "#332D40", + "25": "#3E384C", + "30": "#4A4458", + "35": "#564F64", + "40": "#625B70", + "50": "#7B748A", + "60": "#958DA4", + "70": "#B0A7BF", + "80": "#CCC2DB", + "90": "#E8DEF8", + "95": "#F6EDFF", + "98": "#FEF7FF", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "tertiary": { + "0": "#000000", + "5": "#240612", + "10": "#31101D", + "15": "#3D1B27", + "20": "#4A2532", + "25": "#56303D", + "30": "#633B48", + "35": "#704654", + "40": "#7E5260", + "50": "#996A78", + "60": "#B58392", + "70": "#D29DAC", + "80": "#EFB8C8", + "90": "#FFD9E3", + "95": "#FFECF0", + "98": "#FFF8F8", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "neutral": { + "0": "#000000", + "5": "#111111", + "10": "#1C1B1B", + "15": "#262526", + "20": "#313030", + "25": "#3C3B3B", + "30": "#484646", + "35": "#545252", + "40": "#605E5E", + "50": "#797676", + "60": "#939090", + "70": "#AEAAAA", + "80": "#C9C6C5", + "90": "#E6E1E1", + "95": "#F4F0EF", + "98": "#FDF8F8", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "neutral-variant": { + "0": "#000000", + "5": "#111112", + "10": "#1C1B1C", + "15": "#262526", + "20": "#313031", + "25": "#3C3B3C", + "30": "#484647", + "35": "#545253", + "40": "#605E5F", + "50": "#797677", + "60": "#939091", + "70": "#AEAAAB", + "80": "#C9C5C6", + "90": "#E6E1E2", + "95": "#F4F0F0", + "98": "#FDF8F9", + "99": "#FFFBFF", + "100": "#FFFFFF" + } + } +} \ No newline at end of file diff --git a/ui/themes/material_red_theme.json b/ui/themes/material_red_theme.json new file mode 100644 index 0000000..7b21f1d --- /dev/null +++ b/ui/themes/material_red_theme.json @@ -0,0 +1,418 @@ +{ + "description": "TYPE: CUSTOM\nMaterial Theme Builder export 2025-08-29 10:19:37", + "seed": "#B33B15", + "coreColors": { + "primary": "#B33B15" + }, + "extendedColors": [], + "schemes": { + "light": { + "primary": "#8F4C38", + "surfaceTint": "#8F4C38", + "onPrimary": "#FFFFFF", + "primaryContainer": "#FFDBD1", + "onPrimaryContainer": "#723523", + "secondary": "#77574E", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#FFDBD1", + "onSecondaryContainer": "#5D4037", + "tertiary": "#6C5D2F", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#F5E1A7", + "onTertiaryContainer": "#534619", + "error": "#BA1A1A", + "onError": "#FFFFFF", + "errorContainer": "#FFDAD6", + "onErrorContainer": "#93000A", + "background": "#FFF8F6", + "onBackground": "#231917", + "surface": "#FFF8F6", + "onSurface": "#231917", + "surfaceVariant": "#F5DED8", + "onSurfaceVariant": "#53433F", + "outline": "#85736E", + "outlineVariant": "#D8C2BC", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#392E2B", + "inverseOnSurface": "#FFEDE8", + "inversePrimary": "#FFB5A0", + "primaryFixed": "#FFDBD1", + "onPrimaryFixed": "#3A0B01", + "primaryFixedDim": "#FFB5A0", + "onPrimaryFixedVariant": "#723523", + "secondaryFixed": "#FFDBD1", + "onSecondaryFixed": "#2C150F", + "secondaryFixedDim": "#E7BDB2", + "onSecondaryFixedVariant": "#5D4037", + "tertiaryFixed": "#F5E1A7", + "onTertiaryFixed": "#231B00", + "tertiaryFixedDim": "#D8C58D", + "onTertiaryFixedVariant": "#534619", + "surfaceDim": "#E8D6D2", + "surfaceBright": "#FFF8F6", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#FFF1ED", + "surfaceContainer": "#FCEAE5", + "surfaceContainerHigh": "#F7E4E0", + "surfaceContainerHighest": "#F1DFDA" + }, + "light-medium-contrast": { + "primary": "#5D2514", + "surfaceTint": "#8F4C38", + "onPrimary": "#FFFFFF", + "primaryContainer": "#A15A45", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#4B2F28", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#87655C", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#41350A", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#7B6C3C", + "onTertiaryContainer": "#FFFFFF", + "error": "#740006", + "onError": "#FFFFFF", + "errorContainer": "#CF2C27", + "onErrorContainer": "#FFFFFF", + "background": "#FFF8F6", + "onBackground": "#231917", + "surface": "#FFF8F6", + "onSurface": "#180F0D", + "surfaceVariant": "#F5DED8", + "onSurfaceVariant": "#41332F", + "outline": "#5F4F4A", + "outlineVariant": "#7B6964", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#392E2B", + "inverseOnSurface": "#FFEDE8", + "inversePrimary": "#FFB5A0", + "primaryFixed": "#A15A45", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#84422F", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#87655C", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#6D4D45", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#7B6C3C", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#615426", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#D4C3BE", + "surfaceBright": "#FFF8F6", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#FFF1ED", + "surfaceContainer": "#F7E4E0", + "surfaceContainerHigh": "#EBD9D4", + "surfaceContainerHighest": "#DFCEC9" + }, + "light-high-contrast": { + "primary": "#501B0B", + "surfaceTint": "#8F4C38", + "onPrimary": "#FFFFFF", + "primaryContainer": "#753725", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#3F261E", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#60423A", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#362B02", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#55481C", + "onTertiaryContainer": "#FFFFFF", + "error": "#600004", + "onError": "#FFFFFF", + "errorContainer": "#98000A", + "onErrorContainer": "#FFFFFF", + "background": "#FFF8F6", + "onBackground": "#231917", + "surface": "#FFF8F6", + "onSurface": "#000000", + "surfaceVariant": "#F5DED8", + "onSurfaceVariant": "#000000", + "outline": "#372925", + "outlineVariant": "#554641", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#392E2B", + "inverseOnSurface": "#FFFFFF", + "inversePrimary": "#FFB5A0", + "primaryFixed": "#753725", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#592111", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#60423A", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#472C24", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#55481C", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#3D3206", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#C6B5B1", + "surfaceBright": "#FFF8F6", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#FFEDE8", + "surfaceContainer": "#F1DFDA", + "surfaceContainerHigh": "#E2D1CC", + "surfaceContainerHighest": "#D4C3BE" + }, + "dark": { + "primary": "#FFB5A0", + "surfaceTint": "#FFB5A0", + "onPrimary": "#561F0F", + "primaryContainer": "#723523", + "onPrimaryContainer": "#FFDBD1", + "secondary": "#E7BDB2", + "onSecondary": "#442A22", + "secondaryContainer": "#5D4037", + "onSecondaryContainer": "#FFDBD1", + "tertiary": "#D8C58D", + "onTertiary": "#3B2F05", + "tertiaryContainer": "#534619", + "onTertiaryContainer": "#F5E1A7", + "error": "#FFB4AB", + "onError": "#690005", + "errorContainer": "#93000A", + "onErrorContainer": "#FFDAD6", + "background": "#1A110F", + "onBackground": "#F1DFDA", + "surface": "#1A110F", + "onSurface": "#F1DFDA", + "surfaceVariant": "#53433F", + "onSurfaceVariant": "#D8C2BC", + "outline": "#A08C87", + "outlineVariant": "#53433F", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#F1DFDA", + "inverseOnSurface": "#392E2B", + "inversePrimary": "#8F4C38", + "primaryFixed": "#FFDBD1", + "onPrimaryFixed": "#3A0B01", + "primaryFixedDim": "#FFB5A0", + "onPrimaryFixedVariant": "#723523", + "secondaryFixed": "#FFDBD1", + "onSecondaryFixed": "#2C150F", + "secondaryFixedDim": "#E7BDB2", + "onSecondaryFixedVariant": "#5D4037", + "tertiaryFixed": "#F5E1A7", + "onTertiaryFixed": "#231B00", + "tertiaryFixedDim": "#D8C58D", + "onTertiaryFixedVariant": "#534619", + "surfaceDim": "#1A110F", + "surfaceBright": "#423734", + "surfaceContainerLowest": "#140C0A", + "surfaceContainerLow": "#231917", + "surfaceContainer": "#271D1B", + "surfaceContainerHigh": "#322825", + "surfaceContainerHighest": "#3D322F" + }, + "dark-medium-contrast": { + "primary": "#FFD2C6", + "surfaceTint": "#FFB5A0", + "onPrimary": "#481506", + "primaryContainer": "#CB7C65", + "onPrimaryContainer": "#000000", + "secondary": "#FED3C7", + "onSecondary": "#381F18", + "secondaryContainer": "#AE887E", + "onSecondaryContainer": "#000000", + "tertiary": "#EFDBA1", + "onTertiary": "#2F2500", + "tertiaryContainer": "#A0905C", + "onTertiaryContainer": "#000000", + "error": "#FFD2CC", + "onError": "#540003", + "errorContainer": "#FF5449", + "onErrorContainer": "#000000", + "background": "#1A110F", + "onBackground": "#F1DFDA", + "surface": "#1A110F", + "onSurface": "#FFFFFF", + "surfaceVariant": "#53433F", + "onSurfaceVariant": "#EED7D1", + "outline": "#C2ADA8", + "outlineVariant": "#A08C87", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#F1DFDA", + "inverseOnSurface": "#322825", + "inversePrimary": "#743624", + "primaryFixed": "#FFDBD1", + "onPrimaryFixed": "#280500", + "primaryFixedDim": "#FFB5A0", + "onPrimaryFixedVariant": "#5D2514", + "secondaryFixed": "#FFDBD1", + "onSecondaryFixed": "#200B06", + "secondaryFixedDim": "#E7BDB2", + "onSecondaryFixedVariant": "#4B2F28", + "tertiaryFixed": "#F5E1A7", + "onTertiaryFixed": "#171100", + "tertiaryFixedDim": "#D8C58D", + "onTertiaryFixedVariant": "#41350A", + "surfaceDim": "#1A110F", + "surfaceBright": "#4E423F", + "surfaceContainerLowest": "#0D0604", + "surfaceContainerLow": "#251B19", + "surfaceContainer": "#302623", + "surfaceContainerHigh": "#3B302D", + "surfaceContainerHighest": "#463B38" + }, + "dark-high-contrast": { + "primary": "#FFECE7", + "surfaceTint": "#FFB5A0", + "onPrimary": "#000000", + "primaryContainer": "#FFAF98", + "onPrimaryContainer": "#1E0300", + "secondary": "#FFECE7", + "onSecondary": "#000000", + "secondaryContainer": "#E3B9AE", + "onSecondaryContainer": "#190603", + "tertiary": "#FFEFC4", + "onTertiary": "#000000", + "tertiaryContainer": "#D4C289", + "onTertiaryContainer": "#100B00", + "error": "#FFECE9", + "onError": "#000000", + "errorContainer": "#FFAEA4", + "onErrorContainer": "#220001", + "background": "#1A110F", + "onBackground": "#F1DFDA", + "surface": "#1A110F", + "onSurface": "#FFFFFF", + "surfaceVariant": "#53433F", + "onSurfaceVariant": "#FFFFFF", + "outline": "#FFECE7", + "outlineVariant": "#D4BEB8", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#F1DFDA", + "inverseOnSurface": "#000000", + "inversePrimary": "#743624", + "primaryFixed": "#FFDBD1", + "onPrimaryFixed": "#000000", + "primaryFixedDim": "#FFB5A0", + "onPrimaryFixedVariant": "#280500", + "secondaryFixed": "#FFDBD1", + "onSecondaryFixed": "#000000", + "secondaryFixedDim": "#E7BDB2", + "onSecondaryFixedVariant": "#200B06", + "tertiaryFixed": "#F5E1A7", + "onTertiaryFixed": "#000000", + "tertiaryFixedDim": "#D8C58D", + "onTertiaryFixedVariant": "#171100", + "surfaceDim": "#1A110F", + "surfaceBright": "#5A4D4A", + "surfaceContainerLowest": "#000000", + "surfaceContainerLow": "#271D1B", + "surfaceContainer": "#392E2B", + "surfaceContainerHigh": "#443936", + "surfaceContainerHighest": "#504441" + } + }, + "palettes": { + "primary": { + "0": "#000000", + "5": "#280500", + "10": "#3B0900", + "15": "#4D0F00", + "20": "#5F1500", + "25": "#731B00", + "30": "#862200", + "35": "#9A2902", + "40": "#AB350F", + "50": "#CD4D26", + "60": "#EF663D", + "70": "#FF8B69", + "80": "#FFB5A0", + "90": "#FFDBD1", + "95": "#FFEDE8", + "98": "#FFF8F6", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "secondary": { + "0": "#000000", + "5": "#240803", + "10": "#311309", + "15": "#3E1C13", + "20": "#4A271C", + "25": "#573126", + "30": "#643C31", + "35": "#72483C", + "40": "#7F5347", + "50": "#9B6C5E", + "60": "#B88576", + "70": "#D59F8F", + "80": "#F2B9A9", + "90": "#FFDBD1", + "95": "#FFEDE8", + "98": "#FFF8F6", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "tertiary": { + "0": "#000000", + "5": "#161000", + "10": "#231B00", + "15": "#2F2500", + "20": "#3B2F00", + "25": "#483A00", + "30": "#564500", + "35": "#635109", + "40": "#705D16", + "50": "#8A752D", + "60": "#A58F44", + "70": "#C1AA5C", + "80": "#DEC574", + "90": "#FCE18D", + "95": "#FFF0C8", + "98": "#FFF8F0", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "neutral": { + "0": "#000000", + "5": "#15100E", + "10": "#201A18", + "15": "#2B2422", + "20": "#362F2D", + "25": "#413A38", + "30": "#4D4543", + "35": "#59514E", + "40": "#655C5A", + "50": "#7F7572", + "60": "#998E8C", + "70": "#B4A9A6", + "80": "#D0C4C1", + "90": "#EDE0DC", + "95": "#FBEEEB", + "98": "#FFF8F6", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "neutral-variant": { + "0": "#000000", + "5": "#190E0B", + "10": "#251915", + "15": "#30231F", + "20": "#3B2D29", + "25": "#473834", + "30": "#53433F", + "35": "#5F4F4A", + "40": "#6C5B56", + "50": "#85736E", + "60": "#A08C87", + "70": "#BCA7A1", + "80": "#D8C2BC", + "90": "#F5DED8", + "95": "#FFEDE8", + "98": "#FFF8F6", + "99": "#FFFBFF", + "100": "#FFFFFF" + } + } +} \ No newline at end of file diff --git a/ui/themes/material_slint_theme.json b/ui/themes/material_slint_theme.json new file mode 100644 index 0000000..8204324 --- /dev/null +++ b/ui/themes/material_slint_theme.json @@ -0,0 +1,418 @@ +{ + "description": "TYPE: CUSTOM\nMaterial Theme Builder export 2025-08-29 10:22:12", + "seed": "#2379F4", + "coreColors": { + "primary": "#2379F4" + }, + "extendedColors": [], + "schemes": { + "light": { + "primary": "#445E91", + "surfaceTint": "#445E91", + "onPrimary": "#FFFFFF", + "primaryContainer": "#D8E2FF", + "onPrimaryContainer": "#2B4678", + "secondary": "#575E71", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#DBE2F9", + "onSecondaryContainer": "#3F4759", + "tertiary": "#715573", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#FCD7FB", + "onTertiaryContainer": "#583E5B", + "error": "#BA1A1A", + "onError": "#FFFFFF", + "errorContainer": "#FFDAD6", + "onErrorContainer": "#93000A", + "background": "#F9F9FF", + "onBackground": "#1A1B20", + "surface": "#F9F9FF", + "onSurface": "#1A1B20", + "surfaceVariant": "#E1E2EC", + "onSurfaceVariant": "#44474F", + "outline": "#75777F", + "outlineVariant": "#C4C6D0", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2F3036", + "inverseOnSurface": "#F0F0F7", + "inversePrimary": "#ADC6FF", + "primaryFixed": "#D8E2FF", + "onPrimaryFixed": "#001A42", + "primaryFixedDim": "#ADC6FF", + "onPrimaryFixedVariant": "#2B4678", + "secondaryFixed": "#DBE2F9", + "onSecondaryFixed": "#141B2C", + "secondaryFixedDim": "#BFC6DC", + "onSecondaryFixedVariant": "#3F4759", + "tertiaryFixed": "#FCD7FB", + "onTertiaryFixed": "#29132D", + "tertiaryFixedDim": "#DEBCDF", + "onTertiaryFixedVariant": "#583E5B", + "surfaceDim": "#D9D9E0", + "surfaceBright": "#F9F9FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F3F3FA", + "surfaceContainer": "#EEEDF4", + "surfaceContainerHigh": "#E8E7EF", + "surfaceContainerHighest": "#E2E2E9" + }, + "light-medium-contrast": { + "primary": "#183566", + "surfaceTint": "#445E91", + "onPrimary": "#FFFFFF", + "primaryContainer": "#536DA1", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#2E3647", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#656D80", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#462D49", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#816383", + "onTertiaryContainer": "#FFFFFF", + "error": "#740006", + "onError": "#FFFFFF", + "errorContainer": "#CF2C27", + "onErrorContainer": "#FFFFFF", + "background": "#F9F9FF", + "onBackground": "#1A1B20", + "surface": "#F9F9FF", + "onSurface": "#0F1116", + "surfaceVariant": "#E1E2EC", + "onSurfaceVariant": "#33363E", + "outline": "#50525A", + "outlineVariant": "#6A6D75", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2F3036", + "inverseOnSurface": "#F0F0F7", + "inversePrimary": "#ADC6FF", + "primaryFixed": "#536DA1", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#3A5487", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#656D80", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#4D5567", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#816383", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#674C69", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#C6C6CD", + "surfaceBright": "#F9F9FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F3F3FA", + "surfaceContainer": "#E8E7EF", + "surfaceContainerHigh": "#DCDCE3", + "surfaceContainerHighest": "#D1D1D8" + }, + "light-high-contrast": { + "primary": "#0A2B5B", + "surfaceTint": "#445E91", + "onPrimary": "#FFFFFF", + "primaryContainer": "#2E487A", + "onPrimaryContainer": "#FFFFFF", + "secondary": "#242C3D", + "onSecondary": "#FFFFFF", + "secondaryContainer": "#41495B", + "onSecondaryContainer": "#FFFFFF", + "tertiary": "#3B233F", + "onTertiary": "#FFFFFF", + "tertiaryContainer": "#5A405D", + "onTertiaryContainer": "#FFFFFF", + "error": "#600004", + "onError": "#FFFFFF", + "errorContainer": "#98000A", + "onErrorContainer": "#FFFFFF", + "background": "#F9F9FF", + "onBackground": "#1A1B20", + "surface": "#F9F9FF", + "onSurface": "#000000", + "surfaceVariant": "#E1E2EC", + "onSurfaceVariant": "#000000", + "outline": "#292C33", + "outlineVariant": "#464951", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#2F3036", + "inverseOnSurface": "#FFFFFF", + "inversePrimary": "#ADC6FF", + "primaryFixed": "#2E487A", + "onPrimaryFixed": "#FFFFFF", + "primaryFixedDim": "#143162", + "onPrimaryFixedVariant": "#FFFFFF", + "secondaryFixed": "#41495B", + "onSecondaryFixed": "#FFFFFF", + "secondaryFixedDim": "#2B3344", + "onSecondaryFixedVariant": "#FFFFFF", + "tertiaryFixed": "#5A405D", + "onTertiaryFixed": "#FFFFFF", + "tertiaryFixedDim": "#422A46", + "onTertiaryFixedVariant": "#FFFFFF", + "surfaceDim": "#B8B8BF", + "surfaceBright": "#F9F9FF", + "surfaceContainerLowest": "#FFFFFF", + "surfaceContainerLow": "#F0F0F7", + "surfaceContainer": "#E2E2E9", + "surfaceContainerHigh": "#D4D4DB", + "surfaceContainerHighest": "#C6C6CD" + }, + "dark": { + "primary": "#ADC6FF", + "surfaceTint": "#ADC6FF", + "onPrimary": "#112F60", + "primaryContainer": "#2B4678", + "onPrimaryContainer": "#D8E2FF", + "secondary": "#BFC6DC", + "onSecondary": "#293041", + "secondaryContainer": "#3F4759", + "onSecondaryContainer": "#DBE2F9", + "tertiary": "#DEBCDF", + "onTertiary": "#402843", + "tertiaryContainer": "#583E5B", + "onTertiaryContainer": "#FCD7FB", + "error": "#FFB4AB", + "onError": "#690005", + "errorContainer": "#93000A", + "onErrorContainer": "#FFDAD6", + "background": "#111318", + "onBackground": "#E2E2E9", + "surface": "#111318", + "onSurface": "#E2E2E9", + "surfaceVariant": "#44474F", + "onSurfaceVariant": "#C4C6D0", + "outline": "#8E9099", + "outlineVariant": "#44474F", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E2E2E9", + "inverseOnSurface": "#2F3036", + "inversePrimary": "#445E91", + "primaryFixed": "#D8E2FF", + "onPrimaryFixed": "#001A42", + "primaryFixedDim": "#ADC6FF", + "onPrimaryFixedVariant": "#2B4678", + "secondaryFixed": "#DBE2F9", + "onSecondaryFixed": "#141B2C", + "secondaryFixedDim": "#BFC6DC", + "onSecondaryFixedVariant": "#3F4759", + "tertiaryFixed": "#FCD7FB", + "onTertiaryFixed": "#29132D", + "tertiaryFixedDim": "#DEBCDF", + "onTertiaryFixedVariant": "#583E5B", + "surfaceDim": "#111318", + "surfaceBright": "#37393E", + "surfaceContainerLowest": "#0C0E13", + "surfaceContainerLow": "#1A1B20", + "surfaceContainer": "#1E1F25", + "surfaceContainerHigh": "#282A2F", + "surfaceContainerHighest": "#33353A" + }, + "dark-medium-contrast": { + "primary": "#CFDCFF", + "surfaceTint": "#ADC6FF", + "onPrimary": "#012454", + "primaryContainer": "#7790C7", + "onPrimaryContainer": "#000000", + "secondary": "#D5DCF3", + "onSecondary": "#1E2636", + "secondaryContainer": "#8990A5", + "onSecondaryContainer": "#000000", + "tertiary": "#F5D1F5", + "onTertiary": "#341D38", + "tertiaryContainer": "#A687A7", + "onTertiaryContainer": "#000000", + "error": "#FFD2CC", + "onError": "#540003", + "errorContainer": "#FF5449", + "onErrorContainer": "#000000", + "background": "#111318", + "onBackground": "#E2E2E9", + "surface": "#111318", + "onSurface": "#FFFFFF", + "surfaceVariant": "#44474F", + "onSurfaceVariant": "#DADCE6", + "outline": "#B0B1BB", + "outlineVariant": "#8E9099", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E2E2E9", + "inverseOnSurface": "#282A2F", + "inversePrimary": "#2D4779", + "primaryFixed": "#D8E2FF", + "onPrimaryFixed": "#00102E", + "primaryFixedDim": "#ADC6FF", + "onPrimaryFixedVariant": "#183566", + "secondaryFixed": "#DBE2F9", + "onSecondaryFixed": "#091121", + "secondaryFixedDim": "#BFC6DC", + "onSecondaryFixedVariant": "#2E3647", + "tertiaryFixed": "#FCD7FB", + "onTertiaryFixed": "#1E0822", + "tertiaryFixedDim": "#DEBCDF", + "onTertiaryFixedVariant": "#462D49", + "surfaceDim": "#111318", + "surfaceBright": "#43444A", + "surfaceContainerLowest": "#06070C", + "surfaceContainerLow": "#1C1D22", + "surfaceContainer": "#26282D", + "surfaceContainerHigh": "#313238", + "surfaceContainerHighest": "#3C3D43" + }, + "dark-high-contrast": { + "primary": "#ECEFFF", + "surfaceTint": "#ADC6FF", + "onPrimary": "#000000", + "primaryContainer": "#A9C2FC", + "onPrimaryContainer": "#000A22", + "secondary": "#ECEFFF", + "onSecondary": "#000000", + "secondaryContainer": "#BBC2D8", + "onSecondaryContainer": "#040B1B", + "tertiary": "#FFEAFC", + "onTertiary": "#000000", + "tertiaryContainer": "#DAB8DB", + "onTertiaryContainer": "#17031C", + "error": "#FFECE9", + "onError": "#000000", + "errorContainer": "#FFAEA4", + "onErrorContainer": "#220001", + "background": "#111318", + "onBackground": "#E2E2E9", + "surface": "#111318", + "onSurface": "#FFFFFF", + "surfaceVariant": "#44474F", + "onSurfaceVariant": "#FFFFFF", + "outline": "#EEEFF9", + "outlineVariant": "#C1C2CC", + "shadow": "#000000", + "scrim": "#000000", + "inverseSurface": "#E2E2E9", + "inverseOnSurface": "#000000", + "inversePrimary": "#2D4779", + "primaryFixed": "#D8E2FF", + "onPrimaryFixed": "#000000", + "primaryFixedDim": "#ADC6FF", + "onPrimaryFixedVariant": "#00102E", + "secondaryFixed": "#DBE2F9", + "onSecondaryFixed": "#000000", + "secondaryFixedDim": "#BFC6DC", + "onSecondaryFixedVariant": "#091121", + "tertiaryFixed": "#FCD7FB", + "onTertiaryFixed": "#000000", + "tertiaryFixedDim": "#DEBCDF", + "onTertiaryFixedVariant": "#1E0822", + "surfaceDim": "#111318", + "surfaceBright": "#4E5056", + "surfaceContainerLowest": "#000000", + "surfaceContainerLow": "#1E1F25", + "surfaceContainer": "#2F3036", + "surfaceContainerHigh": "#3A3B41", + "surfaceContainerHighest": "#45474C" + } + }, + "palettes": { + "primary": { + "0": "#000000", + "5": "#00102D", + "10": "#001A42", + "15": "#002455", + "20": "#002E6A", + "25": "#00397F", + "30": "#004495", + "35": "#004FAB", + "40": "#005AC2", + "50": "#1572ED", + "60": "#4D8EFF", + "70": "#81AAFF", + "80": "#ADC6FF", + "90": "#D8E2FF", + "95": "#EDF0FF", + "98": "#F9F9FF", + "99": "#FEFBFF", + "100": "#FFFFFF" + }, + "secondary": { + "0": "#000000", + "5": "#031029", + "10": "#0E1B34", + "15": "#19253F", + "20": "#24304A", + "25": "#2F3B56", + "30": "#3A4762", + "35": "#46526E", + "40": "#525E7B", + "50": "#6A7794", + "60": "#8491AF", + "70": "#9EABCB", + "80": "#BAC6E7", + "90": "#D8E2FF", + "95": "#EDF0FF", + "98": "#F9F9FF", + "99": "#FEFBFF", + "100": "#FFFFFF" + }, + "tertiary": { + "0": "#000000", + "5": "#23012A", + "10": "#2F0C36", + "15": "#3A1741", + "20": "#46224C", + "25": "#522D58", + "30": "#5F3964", + "35": "#6B4471", + "40": "#78507D", + "50": "#936897", + "60": "#AE82B2", + "70": "#CB9CCE", + "80": "#E7B6EB", + "90": "#FED6FF", + "95": "#FFEBFC", + "98": "#FFF7FA", + "99": "#FFFBFF", + "100": "#FFFFFF" + }, + "neutral": { + "0": "#000000", + "5": "#101114", + "10": "#1B1B1F", + "15": "#252629", + "20": "#303034", + "25": "#3B3B3F", + "30": "#46464A", + "35": "#525256", + "40": "#5E5E62", + "50": "#77777A", + "60": "#919094", + "70": "#ABABAF", + "80": "#C7C6CA", + "90": "#E3E2E6", + "95": "#F2F0F4", + "98": "#FAF8FD", + "99": "#FEFBFF", + "100": "#FFFFFF" + }, + "neutral-variant": { + "0": "#000000", + "5": "#0E1118", + "10": "#191B22", + "15": "#23262D", + "20": "#2E3038", + "25": "#393B43", + "30": "#44474F", + "35": "#50525A", + "40": "#5C5E66", + "50": "#75777F", + "60": "#8E9099", + "70": "#A9ABB4", + "80": "#C4C6D0", + "90": "#E1E2EC", + "95": "#EFF0FA", + "98": "#F9F9FF", + "99": "#FEFBFF", + "100": "#FFFFFF" + } + } +} \ No newline at end of file diff --git a/ui/ui/components/app_bar.slint b/ui/ui/components/app_bar.slint new file mode 100644 index 0000000..774e15e --- /dev/null +++ b/ui/ui/components/app_bar.slint @@ -0,0 +1,256 @@ +// Copyright © SixtyFPS GmbH +// 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 title; + in property leading_button; + in property show_background; + in property <[IconButtonItem]> trailing_buttons; + in property horizontal_alignment; + in property spacing; + in property vertical_spacing; + in property two_rows; + in property text_style; + in property text_padding; + in property appbar_padding_right: 0; + in property appbar_padding_top: 0; + in property 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 title; + in property leading_button; + in property trailing_button; + in property 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 title; + in property leading_button; + in property <[IconButtonItem]> trailing_buttons; + in property 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 title; + in property leading_button; + in property <[IconButtonItem]> trailing_buttons; + in property 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 title; + in property leading_button; + in property <[IconButtonItem]> trailing_buttons; + in property 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); + } + } + } +} diff --git a/ui/ui/components/badge.slint b/ui/ui/components/badge.slint new file mode 100644 index 0000000..eacae3e --- /dev/null +++ b/ui/ui/components/badge.slint @@ -0,0 +1,36 @@ +// Copyright © SixtyFPS GmbH +// 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 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; + } + ] +} diff --git a/ui/ui/components/base_button.slint b/ui/ui/components/base_button.slint new file mode 100644 index 0000000..950e734 --- /dev/null +++ b/ui/ui/components/base_button.slint @@ -0,0 +1,72 @@ +// Copyright © SixtyFPS GmbH +// 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 icon; + in property icon_color: self.color; + in property text; + in property button_horizontal_padding: MaterialStyleMetrics.padding_24; + in property button_vertical_padding: MaterialStyleMetrics.padding_10; + in property button_padding_left: self.button_horizontal_padding; + in property button_padding_right: self.button_horizontal_padding; + in property button_padding_top: self.button_vertical_padding; + in property button_padding_bottom: self.button_vertical_padding; + in property spacing: MaterialStyleMetrics.spacing_8; + in property min_layout_width: MaterialStyleMetrics.size_40; + in property min_layout_height: MaterialStyleMetrics.size_40; + in property icon_size: MaterialStyleMetrics.icon_size_18; + in property avatar_icon; + in property avatar_size; + in property avatar_background: #00000000; + out property has_icon: root.icon.width > 0 && root.icon.height > 0; + out property 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 + } +} diff --git a/ui/ui/components/base_navigation.slint b/ui/ui/components/base_navigation.slint new file mode 100644 index 0000000..a8a4100 --- /dev/null +++ b/ui/ui/components/base_navigation.slint @@ -0,0 +1,51 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { NavigationItem } from "../items/navigation_item.slint"; + +export component BaseNavigationItemTemplate { + in property icon; + in property selected_icon; + in property text; + in property index; + in property selected; + in property show_badge; + in property 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 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); + } + +} diff --git a/ui/ui/components/bottom_app_bar.slint b/ui/ui/components/bottom_app_bar.slint new file mode 100644 index 0000000..daea6ac --- /dev/null +++ b/ui/ui/components/bottom_app_bar.slint @@ -0,0 +1,64 @@ +// Copyright © SixtyFPS GmbH +// 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 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(); + } + } + } + } + } +} diff --git a/ui/ui/components/bottom_sheet.slint b/ui/ui/components/bottom_sheet.slint new file mode 100644 index 0000000..0883352 --- /dev/null +++ b/ui/ui/components/bottom_sheet.slint @@ -0,0 +1,125 @@ +// Copyright © SixtyFPS GmbH +// 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 drag_margin: MaterialStyleMetrics.padding_56; + + modal := Modal { + width: 100%; + height: 100%; + + sheet := BottomSheet { + property y_duration: MaterialAnimations.standard_accelerate_duration; + property y_drag_start; + property 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; + } + } +} diff --git a/ui/ui/components/card.slint b/ui/ui/components/card.slint new file mode 100644 index 0000000..6afebef --- /dev/null +++ b/ui/ui/components/card.slint @@ -0,0 +1,112 @@ +// Copyright © SixtyFPS GmbH +// 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 clickable; + in property has_elevation; + in property background; + in property border_color; + in property 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 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 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 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 + } +} diff --git a/ui/ui/components/check_box.slint b/ui/ui/components/check_box.slint new file mode 100644 index 0000000..9d5311b --- /dev/null +++ b/ui/ui/components/check_box.slint @@ -0,0 +1,143 @@ +// Copyright © SixtyFPS GmbH +// 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 check_state; + in_out property tristate; + in property has_error; + in property enabled <=> state_area.enabled; + + property 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; + + init() => { + // Initialize tristate as well + if self.check_state == CheckState.partially-checked { + root.tristate = true; + } + } + + 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 => { + if self.check_state == CheckState.partially-checked { + root.tristate = true; + } + 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; + } + + // Keep for backward compatibility + public function set_check_state(check_state: CheckState) { + 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 check_state <=> check_box.check_state; + in_out property tristate <=> check_box.tristate; + in property 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; + } + } + + // When clicked on this, but not on the checkbox + // apply the click on the checkbox manually + // So the user can click anywhere in the tile to toggle + // the checkbox + clicked => { + check_box.toggle(); + } + + // Keep function for backward compatibility + public function set_check_state(check-state: CheckState) { + check_box.set_check_state(check_state); + } +} diff --git a/ui/ui/components/chip.slint b/ui/ui/components/chip.slint new file mode 100644 index 0000000..4182279 --- /dev/null +++ b/ui/ui/components/chip.slint @@ -0,0 +1,205 @@ +// Copyright © SixtyFPS GmbH +// 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 icon <=> base.icon; + in property text <=> base.text; + in property enabled <=> base.enabled; + in property tooltip <=> base.tooltip; + in property avatar_icon <=> base.avatar-icon; + in property 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 icon <=> base.icon; + in property text <=> base.text; + in property enabled <=> base.enabled; + in property tooltip <=> base.tooltip; + in_out property 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 leading_icon <=> base.icon; + in property trailing_icon; + in property avatar <=> base.avatar-icon; + in property avatar_background <=> base.avatar_background; + in property text <=> base.text; + in property enabled <=> base.enabled; + in property tooltip <=> base.tooltip; + in property checkable; + in_out property checked; + + property 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; + } + ] +} + diff --git a/ui/ui/components/date_picker.slint b/ui/ui/components/date_picker.slint new file mode 100644 index 0000000..4b9538b --- /dev/null +++ b/ui/ui/components/date_picker.slint @@ -0,0 +1,566 @@ +// Copyright © SixtyFPS GmbH +// 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 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 selected; + in property today; + in property text <=> text_label.text; + in property 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 column_count; + in property row_count; + in property delegate_size; + in property start_column; + in property <[string]> header_model; + in property month_count; + in property today; + in property selected_date; + in property display_month; + in property 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 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 spacing; + in property visible_row_count; + in property column_count; + in property delegate_width; + in property delegate_height; + in property selected_year; + in property today_year; + + callback select_year(year: int); + + property row_height: root.height / root.visible_row_count; + property row_count: root.model.length / root.column_count; + property viewport_height: root.row_count * root.row_height; + property start_x: root.width / (root.column_count + 1); + property 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 text <=> text-label.text; + in property icon <=> icon-image.source; + in property enabled: true; + in-out property 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; + transform-rotation: root.checked ? 180deg : 0; + + animate transform-rotation { duration: 250ms; } + } + } + + states [ + disabled when !root.enabled : { + root.opacity: 0.38; + } + ] +} + + +export component DatePickerPopup inherits PopupWindow { + in property title; + in property 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 input_title: @tr("Enter date"); + property input_placeholder_text: "mm/dd/yyyy"; + property input_format: "%m/%d/%Y"; + property cancel_text: @tr("Cancel"); + property ok_text: @tr("Ok"); + property display_date: root.date; + property current_date: root.date; + property delegate_size: 40px; + property year_delegate_width: 72px; + property calendar_column_count: 7; + property calendar_row_count: 6; + property calendar_min_width: root.delegate_size * root.calendar_column_count + (root.calendar_column_count - 1) * 1px; + property calendar_min_height: root.delegate_size *(root.calendar_row_count + 1) + (root.calendar_row_count - 1) * 1px; + property year_selection_column_count: 3; + property year_selection_row_count: 5; + property year_selection; + property selection_mode: true; + property current_input; + + property 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 start_column: DatePickerAdapter.month_offset(root.display_date.month, root.display_date.year); + property 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 selected_year: root.display_date.year; + property today_year: root.today[2]; + property 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; + } +} diff --git a/ui/ui/components/dialog.slint b/ui/ui/components/dialog.slint new file mode 100644 index 0000000..fcdd916 --- /dev/null +++ b/ui/ui/components/dialog.slint @@ -0,0 +1,233 @@ +// Copyright © SixtyFPS GmbH +// 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 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 title; + in property icon; + in property <[image]> action_button_icons; + in property default_action_text; + in property <[string]> actions; + + property 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 title <=> base.title; + in property icon <=> base.icon; + in property 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(); + } + } +} diff --git a/ui/ui/components/divider.slint b/ui/ui/components/divider.slint new file mode 100644 index 0000000..c7b9498 --- /dev/null +++ b/ui/ui/components/divider.slint @@ -0,0 +1,28 @@ +// Copyright © SixtyFPS GmbH +// 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%; + } +} diff --git a/ui/ui/components/drawer.slint b/ui/ui/components/drawer.slint new file mode 100644 index 0000000..5938d44 --- /dev/null +++ b/ui/ui/components/drawer.slint @@ -0,0 +1,89 @@ +// Copyright © SixtyFPS GmbH +// 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 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 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 position_right; + in property 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; + } + } +} diff --git a/ui/ui/components/drop_down_menu.slint b/ui/ui/components/drop_down_menu.slint new file mode 100644 index 0000000..7f96101 --- /dev/null +++ b/ui/ui/components/drop_down_menu.slint @@ -0,0 +1,125 @@ +// Copyright © SixtyFPS GmbH +// 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 label <=> text_field.label; + in property enabled <=> text_field.enabled; + in property leading_icon <=> text_field.leading_icon; + in property <[MenuItem]> items; + in_out property current_index: -1; + + callback selected(index: int); + + property item_height: MaterialStyleMetrics.size_56; + property max_displayed_items: 6; + property 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); + } +} diff --git a/ui/ui/components/elevated_button.slint b/ui/ui/components/elevated_button.slint new file mode 100644 index 0000000..fbfb809 --- /dev/null +++ b/ui/ui/components/elevated_button.slint @@ -0,0 +1,45 @@ +// Copyright © SixtyFPS GmbH +// 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 icon <=> base.icon; + in property text <=> base.text; + in property tooltip <=> base.tooltip; + in property 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; + } + ] +} diff --git a/ui/ui/components/elevation.slint b/ui/ui/components/elevation.slint new file mode 100644 index 0000000..9fc8596 --- /dev/null +++ b/ui/ui/components/elevation.slint @@ -0,0 +1,108 @@ +// Copyright © SixtyFPS GmbH +// 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 background; + in property border_radius; + in property level; + + property 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; + } + ] +} diff --git a/ui/ui/components/extended_touch_area.slint b/ui/ui/components/extended_touch_area.slint new file mode 100644 index 0000000..5108c7c --- /dev/null +++ b/ui/ui/components/extended_touch_area.slint @@ -0,0 +1,57 @@ +// Copyright © SixtyFPS GmbH +// 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 tooltip; + in property tooltip_offset; + out property has_focus <=> focus_scope.has_focus; + out property 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; + } +} diff --git a/ui/ui/components/filled_button.slint b/ui/ui/components/filled_button.slint new file mode 100644 index 0000000..e2b503f --- /dev/null +++ b/ui/ui/components/filled_button.slint @@ -0,0 +1,43 @@ +// Copyright © SixtyFPS GmbH +// 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 icon <=> base.icon; + in property text <=> base.text; + in property tooltip <=> base.tooltip; + in property 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; + } + ] +} diff --git a/ui/ui/components/filled_icon_button.slint b/ui/ui/components/filled_icon_button.slint new file mode 100644 index 0000000..644202e --- /dev/null +++ b/ui/ui/components/filled_icon_button.slint @@ -0,0 +1,64 @@ +// Copyright © SixtyFPS GmbH +// 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 icon; + in property checked_icon: root.icon; + in property checkable; + in_out property checked; + in property tooltip <=> base.tooltip; + in property enabled <=> base.enabled; + + callback clicked(); + + accessible-role: button; + accessible-enabled: root.enabled; + 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; + } + ] +} diff --git a/ui/ui/components/floating_action_button.slint b/ui/ui/components/floating_action_button.slint new file mode 100644 index 0000000..32077f7 --- /dev/null +++ b/ui/ui/components/floating_action_button.slint @@ -0,0 +1,55 @@ +// Copyright © SixtyFPS GmbH +// 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 icon <=> base.icon; + in property text <=> base.text; + in property tooltip <=> base.tooltip; + in property 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; + } + ] +} diff --git a/ui/ui/components/grid.slint b/ui/ui/components/grid.slint new file mode 100644 index 0000000..9c82f56 --- /dev/null +++ b/ui/ui/components/grid.slint @@ -0,0 +1,10 @@ +// Copyright © SixtyFPS GmbH +// 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; +} diff --git a/ui/ui/components/horizontal.slint b/ui/ui/components/horizontal.slint new file mode 100644 index 0000000..bddf801 --- /dev/null +++ b/ui/ui/components/horizontal.slint @@ -0,0 +1,9 @@ +// Copyright © SixtyFPS GmbH +// 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; +} diff --git a/ui/ui/components/icon.slint b/ui/ui/components/icon.slint new file mode 100644 index 0000000..4d23105 --- /dev/null +++ b/ui/ui/components/icon.slint @@ -0,0 +1,11 @@ +// Copyright © SixtyFPS GmbH +// 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; +} diff --git a/ui/ui/components/icon_button.slint b/ui/ui/components/icon_button.slint new file mode 100644 index 0000000..2acae9d --- /dev/null +++ b/ui/ui/components/icon_button.slint @@ -0,0 +1,62 @@ +// Copyright © SixtyFPS GmbH +// 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 icon; + in property checked_icon: root.icon; + in property checkable; + in_out property checked; + in property tooltip <=> base.tooltip; + in property enabled <=> base.enabled; + in property inverse; + in property inline: false; + in property has_error; + + callback clicked(); + + accessible-role: button; + accessible-enabled: root.enabled; + 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; + } + ] +} diff --git a/ui/ui/components/list.slint b/ui/ui/components/list.slint new file mode 100644 index 0000000..8e7bd68 --- /dev/null +++ b/ui/ui/components/list.slint @@ -0,0 +1,107 @@ +// Copyright © SixtyFPS GmbH +// 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; + in property 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 text; + in property supporting_text; + in property avatar_icon; + in property avatar_text; + in property avatar_background; + in property avatar_foreground; + in property enabled <=> state_layer.enabled; + + property 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%; + } + ] +} + diff --git a/ui/ui/components/list_view.slint b/ui/ui/components/list_view.slint new file mode 100644 index 0000000..aeb847f --- /dev/null +++ b/ui/ui/components/list_view.slint @@ -0,0 +1,8 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { ScrollView } from "./scroll_view.slint"; + +export component ListView inherits ScrollView { + +} diff --git a/ui/ui/components/material_text.slint b/ui/ui/components/material_text.slint new file mode 100644 index 0000000..4aa5c20 --- /dev/null +++ b/ui/ui/components/material_text.slint @@ -0,0 +1,14 @@ +// Copyright © SixtyFPS GmbH +// 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 style: MaterialTypography.body_medium; + + font_size: root.style.font_size; + font_weight: root.style.font_weight; + color: MaterialPalette.on_background; + overflow: elide; +} diff --git a/ui/ui/components/material_window.slint b/ui/ui/components/material_window.slint new file mode 100644 index 0000000..a861ac0 --- /dev/null +++ b/ui/ui/components/material_window.slint @@ -0,0 +1,18 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { MaterialPalette } from "../styling/material_palette.slint"; + +export global MaterialWindowAdapter { + in property disable_hover; +} + +export component MaterialWindow inherits Window { + in property disable_hover; + + background: MaterialPalette.background; + + changed disable_hover => { + MaterialWindowAdapter.disable_hover = root.disable_hover; + } +} diff --git a/ui/ui/components/menu.slint b/ui/ui/components/menu.slint new file mode 100644 index 0000000..f04da7a --- /dev/null +++ b/ui/ui/components/menu.slint @@ -0,0 +1,136 @@ +// Copyright © SixtyFPS GmbH +// 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 icon; + in property text; + in property trailing_text; + in property enabled <=> state_area.enabled; + in property selected; + + callback clicked(); + + property has_icon: root.icon.width > 0 && root.icon.height > 0; + property color: MaterialPalette.on_surface; + property 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 current_index: -1; + + callback activated(index: int); + + // used to fix clipping of shadows + out property 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); + } + } + } +} diff --git a/ui/ui/components/modal.slint b/ui/ui/components/modal.slint new file mode 100644 index 0000000..b14a4a6 --- /dev/null +++ b/ui/ui/components/modal.slint @@ -0,0 +1,28 @@ +// Copyright © SixtyFPS GmbH +// 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; + } + } +} diff --git a/ui/ui/components/navigation_bar.slint b/ui/ui/components/navigation_bar.slint new file mode 100644 index 0000000..2705ef3 --- /dev/null +++ b/ui/ui/components/navigation_bar.slint @@ -0,0 +1,150 @@ +// Copyright © SixtyFPS GmbH +// 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 has_icon: root.icon.width > 0 && root.icon.height > 0; + property has_selected_icon: root.selected_icon.width > 0 || root.selected_icon.height > 0; + property color: MaterialPalette.on_surface; + + in property navitem_padding_top: MaterialStyleMetrics.padding_12; + in property 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); + } + } + } + } +} diff --git a/ui/ui/components/navigation_drawer.slint b/ui/ui/components/navigation_drawer.slint new file mode 100644 index 0000000..51c92b7 --- /dev/null +++ b/ui/ui/components/navigation_drawer.slint @@ -0,0 +1,227 @@ +// Copyright © SixtyFPS GmbH +// 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 has_icon: root.icon.width > 0 && root.icon.height > 0; + property has_selected_icon: root.selected_icon.width > 0 || root.selected_icon.height > 0; + property 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 title; + in property <[NavigationItem]> items; + in property current_index: -1; + in property 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 current_group; + in_out property 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 }); + } + } + + public 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 { + 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 current_group; + in_out property 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; + + // Workaround for issue #9766 + close_timer := Timer { + interval: 0ms; + running: false; + triggered => { + root.close(); + self.stop(); + } + } + + 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 }); + } + } + + public 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); + close_timer.start(); + } +} diff --git a/ui/ui/components/navigation_rail.slint b/ui/ui/components/navigation_rail.slint new file mode 100644 index 0000000..4ab1a1f --- /dev/null +++ b/ui/ui/components/navigation_rail.slint @@ -0,0 +1,84 @@ +// Copyright © SixtyFPS GmbH +// 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 has_menu; + in property fab_icon; + in property alignment; + + callback menu_clicked(); + callback fab_clicked(); + + min_width: max(MaterialStyleMetrics.size_80, layout.min_width); + + Rectangle { + background: MaterialPalette.surface; + + 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); + } + } + } + } + } + } +} diff --git a/ui/ui/components/outline_button.slint b/ui/ui/components/outline_button.slint new file mode 100644 index 0000000..e40db92 --- /dev/null +++ b/ui/ui/components/outline_button.slint @@ -0,0 +1,42 @@ +// Copyright © SixtyFPS GmbH +// 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 icon <=> base.icon; + in property text <=> base.text; + in property enabled <=> base.enabled; + in property 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; + } + ] +} diff --git a/ui/ui/components/outline_icon_button.slint b/ui/ui/components/outline_icon_button.slint new file mode 100644 index 0000000..263df34 --- /dev/null +++ b/ui/ui/components/outline_icon_button.slint @@ -0,0 +1,78 @@ +// Copyright © SixtyFPS GmbH +// 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 icon; + in property checked_icon: root.icon; + in property checkable; + in_out property checked; + in property tooltip <=> base.tooltip; + in property enabled <=> base.enabled; + + callback clicked(); + + accessible-role: button; + accessible-enabled: root.enabled; + 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; + } + ] +} diff --git a/ui/ui/components/progress_indicator.slint b/ui/ui/components/progress_indicator.slint new file mode 100644 index 0000000..92ded2c --- /dev/null +++ b/ui/ui/components/progress_indicator.slint @@ -0,0 +1,115 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint"; +import { MaterialPalette } from "../styling/material_palette.slint"; + +export component LinearProgressIndicator { + in property progress; + in property indeterminate; + + accessible-role: progress-indicator; + accessible-value: !root.indeterminate ? root.progress : ""; + accessible-value-minimum: 0.0; + accessible-value-maximum: 1.0; + 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 progress; + in property indeterminate; + + property bar_height: MaterialStyleMetrics.size_4; + + accessible-role: progress-indicator; + accessible-value: !root.indeterminate ? root.progress : ""; + accessible-value-minimum: 0.0; + accessible-value-maximum: 1.0; + 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 radius: min(self.viewbox_width, self.viewbox_height) / 2; + property start_x: self.viewbox_width / 2; + property start_y: self.viewbox_height / 2; + property inner_radius: self.radius - (root.bar_height * (self.viewbox_height / self.height)); + property start: !root.indeterminate ? 0 : 1 * mod(animation-tick(), 1.5s) / 1.5s; + property 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); + } + } +} diff --git a/ui/ui/components/radio_button.slint b/ui/ui/components/radio_button.slint new file mode 100644 index 0000000..2d6a1b1 --- /dev/null +++ b/ui/ui/components/radio_button.slint @@ -0,0 +1,99 @@ +// Copyright © SixtyFPS GmbH +// 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 checked; + in property 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: radio-button; + 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 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(); + } +} diff --git a/ui/ui/components/scroll_view.slint b/ui/ui/components/scroll_view.slint new file mode 100644 index 0000000..dd783c3 --- /dev/null +++ b/ui/ui/components/scroll_view.slint @@ -0,0 +1,166 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { MaterialPalette } from "../styling/material_palette.slint"; + +export component ScrollBar { + in property enabled; + out property has_hover: touch_area.has_hover; + in_out property horizontal; + in_out property maximum; + in_out property page_size; + in_out property value; + in property policy: ScrollBarPolicy.as_needed; + + property track_size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset; + property step_size: 10px; + property offset: 2px; + property 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 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 enabled: true; + out property visible_width <=> flickable.width; + out property visible_height <=> flickable.height; + in_out property viewport_width <=> flickable.viewport_width; + in_out property viewport_height <=> flickable.viewport_height; + in_out property viewport_x <=> flickable.viewport_x; + in_out property viewport_y <=> flickable.viewport_y; + in property vertical_scrollbar_policy <=> vertical_bar.policy; + in property 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 has_focus; + + callback scrolled <=> flickable.flicked; + + property scroll_bar_size: 8px; + property 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; + } +} diff --git a/ui/ui/components/search_bar.slint b/ui/ui/components/search_bar.slint new file mode 100644 index 0000000..211a436 --- /dev/null +++ b/ui/ui/components/search_bar.slint @@ -0,0 +1,324 @@ +// Copyright © SixtyFPS GmbH +// 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 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 icon; + in property 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 text <=> text_input.text; + in property placeholder_text; + property 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 leading_icon: Icons.menu; + in property trailing_icon; + in property avatar_icon; + in property avatar_background: #00000000; + in property placeholder_text; + in property empty_text; + in_out property text; + in_out property 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; + callback item-activated(index: int) -> bool; + + property color: MaterialPalette.on_surface_variant; + property item_height: MaterialStyleMetrics.size_72; + + public function close-popup() { + popup.close(); + } + + 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 => { + if (!root.item-activated(index)) { + 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; + } + } + } +} diff --git a/ui/ui/components/segmented_button.slint b/ui/ui/components/segmented_button.slint new file mode 100644 index 0000000..21acf94 --- /dev/null +++ b/ui/ui/components/segmented_button.slint @@ -0,0 +1,103 @@ +// Copyright © SixtyFPS GmbH +// 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 icon; + in property text; + in property index; + in property selected; + in property 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 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); + } +} diff --git a/ui/ui/components/slider.slint b/ui/ui/components/slider.slint new file mode 100644 index 0000000..6e93f50 --- /dev/null +++ b/ui/ui/components/slider.slint @@ -0,0 +1,155 @@ +// Copyright © SixtyFPS GmbH +// 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 enabled; + in_out property value; + in property minimum; + in property stop_count: 0; + in property maximum: 100; + + callback released(value: float); + callback value_changed(value: float); + + property 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); + accessible-action-set-value(v) => { + if v.is-float() { + root.set_value(v.to-float()); + } + } + accessible-action-increment => { root.increment_value(); } + accessible-action-decrement => { root.decrement_value(); } + 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 > 1: Rectangle { + width: 100%; + height: 100%; + + for i in (root.maximum - root.minimum) / root.steps - 1: 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); + 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 - self.pressed_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.decrement_value(); + return accept; + } + if event.text == Key.RightArrow { + root.increment_value(); + 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) { + if root.steps > 1 { + root.value = clamp(round(value / root.steps) * root.steps, root.minimum, root.maximum) + } else { + root.value = clamp(value, root.minimum, root.maximum) + } + } + + function increment_value() { + root.set_value(root.value + root.steps); + } + + function decrement_value() { + root.set_value(root.value - root.steps); + } +} diff --git a/ui/ui/components/snack_bar.slint b/ui/ui/components/snack_bar.slint new file mode 100644 index 0000000..962c742 --- /dev/null +++ b/ui/ui/components/snack_bar.slint @@ -0,0 +1,88 @@ +// Copyright © SixtyFPS GmbH +// 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 text; + in property action_text; + in property 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(); + } +} diff --git a/ui/ui/components/state_layer.slint b/ui/ui/components/state_layer.slint new file mode 100644 index 0000000..ee50f74 --- /dev/null +++ b/ui/ui/components/state_layer.slint @@ -0,0 +1,171 @@ +// Copyright © SixtyFPS GmbH +// 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 border_radius; + in property pressed_x; + in property pressed_y; + in property color; + in property 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 background; + in property border_radius; + in property display_background: true; + in property enabled; + in property pressed; + in property has_focus; + in property has_hover; + + property 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 has_focus <=> focus_scope.has_focus; + out property enter_pressed; + in property tooltip; + in property 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; + in property border_radius; + in property transparent_background; + in property display_background: true; + in property clip_ripple: true; + + property popup_open; + property 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 +} diff --git a/ui/ui/components/switch.slint b/ui/ui/components/switch.slint new file mode 100644 index 0000000..d92078a --- /dev/null +++ b/ui/ui/components/switch.slint @@ -0,0 +1,151 @@ +// Copyright © SixtyFPS GmbH +// 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 checked; + in property enabled: true; + in property tooltip <=> touch_area.tooltip; + in property on_icon; + in property off_icon; + + property 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 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: switch; + accessible-action-default => { if(root.enabled) {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 indicator-padding: root.checked || root.has_icon ? MaterialStyleMetrics.padding_4 : MaterialStyleMetrics.padding_8; + + property 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; + } + ] +} diff --git a/ui/ui/components/tab_bar.slint b/ui/ui/components/tab_bar.slint new file mode 100644 index 0000000..0be204b --- /dev/null +++ b/ui/ui/components/tab_bar.slint @@ -0,0 +1,210 @@ +// Copyright © SixtyFPS GmbH +// 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 label_width: label.width; + + property 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 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 indicator_width; + property item_width: root.width / root.items.length; + property 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 item_width: root.width / root.items.length; + property 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; + } + } + } +} diff --git a/ui/ui/components/text_button.slint b/ui/ui/components/text_button.slint new file mode 100644 index 0000000..e04e513 --- /dev/null +++ b/ui/ui/components/text_button.slint @@ -0,0 +1,33 @@ +// Copyright © SixtyFPS GmbH +// 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 icon <=> base.icon; + in property text <=> base.text; + in property enabled <=> base.enabled; + in property tooltip <=> base.tooltip; + in property 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; + } + ] +} diff --git a/ui/ui/components/text_field.slint b/ui/ui/components/text_field.slint new file mode 100644 index 0000000..231820d --- /dev/null +++ b/ui/ui/components/text_field.slint @@ -0,0 +1,211 @@ +// Copyright © SixtyFPS GmbH +// 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 label: placeholder_text; + // Deprecated: this property was unintentionally made public. Use `label` instead. + in property placeholder_text; + in property supporting_text; + in property has_error; + in property leading_icon; + in property trailing_icon; + in property enabled: true; + in property read_only <=> input.read_only; + out property has_focus: input.has_focus; + in_out property text <=> input.text; + in property input-type <=> input.input-type; + + 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 has_leading: root.leading_icon.width > 0 && root.leading_icon.height > 0; + property has_trailing: root.trailing_icon.width > 0 && root.trailing_icon.height > 0; + property computed_x; + property 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: 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; + } + ] +} diff --git a/ui/ui/components/time_picker.slint b/ui/ui/components/time_picker.slint new file mode 100644 index 0000000..5109e62 --- /dev/null +++ b/ui/ui/components/time_picker.slint @@ -0,0 +1,674 @@ +// Copyright © SixtyFPS GmbH +// 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 selected; + in property 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 two_columns; + in property total; + in_out property current_index; + in property current_value; + + callback current_index_changed(index: int); + + property radius: max(root.width, root.height) / 2; + property picker_ditameter: MaterialStyleMetrics.size_48; + property center: root.radius - root.picker_ditameter / 2; + property outer_padding: 2px; + property inner_padding: 32px; + property radius_outer: root.center - root.outer_padding; + property radius_inner: root.center - root.inner_padding; + property half_total: root.total / 2; + property rotation: 0.25turn; + property current_x: get_index_x(root.current_value); + property 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 read_only <=> text_input.read_only; + in property checked; + in_out property 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 text <=> label.text; + in property 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 am_selected; + in property 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 use_24_hour_format: true; + in property title: @tr("Select time"); + in property cancel_text: @tr("Cancel"); + in property ok_text: @tr("Ok"); + in property