From 0f2c580809ee3c6f39e7f0e34cf6ff59123307f1 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Thu, 18 Apr 2024 14:24:16 +0200 Subject: [PATCH 01/33] TimePicker: startup --- examples/gallery/ui/pages/controls_page.slint | 4 +- .../widgets/material-base/_keyboard.svg | 1 + .../widgets/material-base/button.slint | 194 ++++++-- .../material-base/std-widgets-base.slint | 2 + .../widgets/material-base/styling.slint | 32 +- .../widgets/material-base/time-picker.slint | 463 ++++++++++++++++++ 6 files changed, 625 insertions(+), 71 deletions(-) create mode 100644 internal/compiler/widgets/material-base/_keyboard.svg create mode 100644 internal/compiler/widgets/material-base/time-picker.slint diff --git a/examples/gallery/ui/pages/controls_page.slint b/examples/gallery/ui/pages/controls_page.slint index 268da5d2f23..de84ec5b0a3 100644 --- a/examples/gallery/ui/pages/controls_page.slint +++ b/examples/gallery/ui/pages/controls_page.slint @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT import { Button, GroupBox, SpinBox, ComboBox, CheckBox, LineEdit, TabWidget, VerticalBox, HorizontalBox, - Slider, ProgressIndicator, SpinBox, Switch, Spinner, GridBox } from "std-widgets.slint"; + Slider, ProgressIndicator, SpinBox, Switch, Spinner, GridBox, TimePicker } from "std-widgets.slint"; import { GallerySettings } from "../gallery_settings.slint"; import { Page } from "page.slint"; @@ -17,6 +17,8 @@ export component ControlsPage inherits Page { VerticalLayout { padding: 0px; + TimePicker {} + HorizontalBox { alignment: start; diff --git a/internal/compiler/widgets/material-base/_keyboard.svg b/internal/compiler/widgets/material-base/_keyboard.svg new file mode 100644 index 00000000000..2d92a39ea06 --- /dev/null +++ b/internal/compiler/widgets/material-base/_keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/compiler/widgets/material-base/button.slint b/internal/compiler/widgets/material-base/button.slint index 5cc1dad191c..4276f36a4a3 100644 --- a/internal/compiler/widgets/material-base/button.slint +++ b/internal/compiler/widgets/material-base/button.slint @@ -4,58 +4,29 @@ import { StateLayer } from "components.slint"; import { MaterialFontSettings, MaterialPalette, Elevation } from "styling.slint"; -// Default button widget with Material Design Filled Button look and feel. -export component Button { +component MaterialButtonBase { in property text; - in property enabled <=> i-state-layer.enabled; - in property checkable; in property icon; - in property primary; + in property border-radius <=> state-layer.border-radius; + in property checkable; + in property text-color; + in property text-opacity; in property colorize-icon; - out property has-focus: i-state-layer.has-focus; - out property pressed: self.enabled && i-state-layer.pressed; + in property enabled <=> state-layer.enabled; + in property layout-padding-left; + in property layout-padding-right; + out property has-focus <=> state-layer.has-focus; + out property pressed <=> state-layer.pressed; in-out property checked; callback clicked; - private property text-color: root.primary ? MaterialPalette.accent-foreground : MaterialPalette.control-foreground; - private property text-opacity: 1.0; - - min-height: max(40px, i-layout.min-height); - min-width: max(40px, i-layout.min-width); - forward-focus: i-state-layer; - - accessible-role: button; - accessible-checkable: root.checkable; - accessible-checked: root.checked; - accessible-label: root.text; - accessible-action-default => { i-state-layer.clicked(); } - - states [ - disabled when !root.enabled : { - i-background.background: MaterialPalette.foreground-alt; - i-background.opacity: 0.12; - root.text-opacity: 0.38; - root.text-color: MaterialPalette.control-foreground; - } - checked when root.checked: { - root.text-color: MaterialPalette.accent-foreground; - } - ] - - i-background := Rectangle { - width: 100%; - height: 100%; - border-radius: 20px; - background: root.primary ? MaterialPalette.accent-background : MaterialPalette.control-background; - drop-shadow-color: transparent; - drop-shadow-blur: Elevation.level0; - drop-shadow-offset-y: 1px; - } + min-width: layout.min-width; + min-height: layout.min-height; + forward-focus: state-layer; - i-state-layer := StateLayer { + state-layer := StateLayer { has-ripple: true; - border-radius: i-background.border-radius; background: MaterialPalette.foreground-alt; ripple-color: MaterialPalette.secondary-ripple; checked-background: MaterialPalette.accent-background; @@ -69,10 +40,10 @@ export component Button { } } - i-layout := HorizontalLayout { - padding-left: 24px; - padding-right: 24px; + layout := HorizontalLayout { spacing: 8px; + padding-left: root.layout-padding-left; + padding-right: root.layout-padding-right; if (root.icon.width > 0 && root.icon.height > 0): Image { source <=> root.icon; @@ -81,7 +52,7 @@ export component Button { colorize: root.colorize-icon ? root.text-color : transparent; } - if (root.text != "") : Text { + if (root.text != ""): Text { text: root.text; color: root.text-color; opacity: root.text-opacity; @@ -90,7 +61,134 @@ export component Button { font-weight: MaterialFontSettings.label-large.font-weight; accessible-role: none; - animate color { duration: 250ms; easing: ease; } + animate color { + duration: 250ms; + easing: ease; + } + } + } +} + +// Default button widget with Material Design Filled Button look and feel. +export component Button { + in property text <=> base.text; + in property enabled <=> base.enabled; + in property checkable <=> base.checkable; + in property icon <=> base.icon; + in property primary; + in property colorize-icon <=> base.colorize-icon; + out property has-focus: base.has-focus; + out property pressed: self.enabled && base.pressed; + in-out property checked <=> base.checked; + + callback clicked <=> base.clicked; + + min-height: max(40px, base.min-height); + min-width: max(40px, base.min-width); + forward-focus: base; + + accessible-role: button; + accessible-checkable: root.checkable; + accessible-checked: root.checked; + accessible-label: root.text; + accessible-action-default => { + base.clicked(); + } + + states [ + disabled when !root.enabled: { + background.background: MaterialPalette.foreground-alt; + background.opacity: 0.12; + base.text-opacity: 0.38; + base.text-color: MaterialPalette.control-foreground; } + checked when root.checked: { + base.text-color: MaterialPalette.accent-foreground; + } + ] + + background := Rectangle { + width: 100%; + height: 100%; + border-radius: 20px; + background: root.primary ? MaterialPalette.accent-background : MaterialPalette.control-background; + drop-shadow-color: transparent; + drop-shadow-blur: Elevation.level0; + drop-shadow-offset-y: 1px; + } + + base := MaterialButtonBase { + layout-padding-left: 24px; + layout-padding-right: 24px; + height: 100%; + border-radius: background.border-radius; + text-color: root.primary ? MaterialPalette.accent-foreground : MaterialPalette.control-foreground; + text-opacity: 1.0; + } +} + +export component TextButton { + in property text <=> base.text; + in property enabled <=> base.enabled; + in property checkable <=> base.checkable; + in property icon <=> base.icon; + + in property colorize-icon <=> base.colorize-icon; + out property has-focus: base.has-focus; + out property pressed: self.enabled && base.pressed; + in-out property checked <=> base.checked; + + callback clicked <=> base.clicked; + + min-height: max(40px, base.min-height); + min-width: max(40px, base.min-width); + forward-focus: base; + + accessible-role: button; + accessible-checkable: root.checkable; + accessible-checked: root.checked; + accessible-label: root.text; + accessible-action-default => { + clicked(); + } + + base := MaterialButtonBase { + layout-padding-left: 12px; + layout-padding-right: 12px; + height: 100%; + border-radius: 20px; + text-color: MaterialPalette.accent-background; + text-opacity: 1.0; + } +} + +export component IconButton { + in property enabled <=> base.enabled; + + in property icon <=> base.icon; + + out property has-focus: base.has-focus; + out property pressed: self.enabled && base.pressed; + + callback clicked <=> base.clicked; + + min-height: max(40px, base.min-height); + min-width: max(40px, base.min-width); + forward-focus: base; + + accessible-role: button; + accessible-action-default => { + clicked(); + } + + base := MaterialButtonBase { + layout-padding-left: 8px; + layout-padding-right: 8px; + width: 100%; + height: 100%; + border-radius: 20px; + text-color: MaterialPalette.control-foreground-variant; + colorize-icon: true; + text-opacity: 1.0; } } diff --git a/internal/compiler/widgets/material-base/std-widgets-base.slint b/internal/compiler/widgets/material-base/std-widgets-base.slint index b962ff72a4b..476b1b7ed50 100644 --- a/internal/compiler/widgets/material-base/std-widgets-base.slint +++ b/internal/compiler/widgets/material-base/std-widgets-base.slint @@ -27,3 +27,5 @@ export { Spinner } import { TextEdit } from "textedit.slint"; export { TextEdit } +import { TimePicker } from "time-picker.slint"; +export { TimePicker } diff --git a/internal/compiler/widgets/material-base/styling.slint b/internal/compiler/widgets/material-base/styling.slint index 2e39c2f93bb..9c01e5a7e16 100644 --- a/internal/compiler/widgets/material-base/styling.slint +++ b/internal/compiler/widgets/material-base/styling.slint @@ -7,30 +7,14 @@ import { ColorSchemeSelector } from "color-scheme.slint"; // typo settings struct TextStyle { font-size: relative-font-size, - font-weight: int -} + font-weight: int} export global MaterialFontSettings { - out property label-large: { - font-size: 14 * 0.0625rem, - font-weight: 500 - }; - out property label-medium: { - font-size: 12 * 0.0625rem, - font-weight: 500 - }; - out property body-large: { - font-size: 16 * 0.0625rem, - font-weight: 400 - }; - out property body-small: { - font-size: 12 * 0.0625rem, - font-weight: 400 - }; - out property title-small: { - font-size: 14 * 0.0625rem, - font-weight: 500 - }; + out property label-large: { font-size: 14 * 0.0625rem, font-weight: 500 }; + out property label-medium: { font-size: 12 * 0.0625rem, font-weight: 500 }; + out property body-large: { font-size: 16 * 0.0625rem, font-weight: 400 }; + out property body-small: { font-size: 12 * 0.0625rem, font-weight: 400 }; + out property title-small: { font-size: 14 * 0.0625rem, font-weight: 500 }; } export global Elevation { @@ -63,6 +47,9 @@ export global MaterialPalette { out property border-variant: !root.dark-color-scheme ? #C4C7C5 : #444746; out property foreground-alt: !root.dark-color-scheme ? #1C1B1F : #E6E1E5; out property secondary-ripple: !root.dark-color-scheme ? #fffc : #000000; + out property surface-container-high: !root.dark-color-scheme ? #ECE6F0 : #2B2930; + out property surface-container-highest: !root.dark-color-scheme ? #E6E0E9 : #36343B; + out property tertiary-container: !root.dark-color-scheme ? #FFD8E4 : #633B48; in-out property color-scheme: ColorSchemeSelector.color-scheme; property dark-color-scheme: { if (color-scheme == ColorScheme.unknown) { @@ -79,4 +66,5 @@ export global Icons { out property arrow-upward: @image-url("_arrow-upward.svg"); out property check-mark: @image-url("_check-mark.svg"); out property expand-more: @image-url("_expand-more.svg"); + out property keyboard: @image-url("_keyboard.svg"); } diff --git a/internal/compiler/widgets/material-base/time-picker.slint b/internal/compiler/widgets/material-base/time-picker.slint new file mode 100644 index 00000000000..396707803a4 --- /dev/null +++ b/internal/compiler/widgets/material-base/time-picker.slint @@ -0,0 +1,463 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial + +import { MaterialPalette, MaterialFontSettings, Icons } from "styling.slint"; +import { VerticalBox } from "layouts.slint"; +import { TextButton, IconButton } from "button.slint"; + +component ValuePicker inherits Rectangle { + in property selected; + in property value; + + callback clicked <=> touch-area.clicked; + + width: max(48px, label.min-width); + height: max(48px, label.min-height); + border-radius: max(root.width, root.height) / 2; + vertical-stretch: 0; + horizontal-stretch: 0; + + touch-area := TouchArea { } + + label := Text { + text: root.value; + vertical-alignment: center; + horizontal-alignment: center; + color: MaterialPalette.foreground; + font-size: MaterialFontSettings.body-large.font-size; + font-weight: MaterialFontSettings.body-large.font-weight; + } + + states [ + selected when root.selected: { + label.color: MaterialPalette.accent-foreground; + } + ] +} + +component Clock inherits Rectangle { + in property <[int]> model; + in property split; + in-out property current-item; + out property current-value: root.model[root.current-item]; + + callback curren-item-changed(/* index */ int); + + property picker-ditameter: 48px; + property center: root.border-radius - root.picker-ditameter / 2; + property outer-padding: 2px; + property inner-padding: 32px; + property radius: root.center - root.outer-padding; + property radius-inner: root.center - root.inner-padding; + property half-model-length: root.model.length / 2; + property rotation: 0.25turn; + + min-width: 256px; + min-height: 256px; + border-radius: max(root.width, root.height) / 2; + vertical-stretch: 0; + horizontal-stretch: 0; + background: MaterialPalette.surface-container-highest; + + if root.current-item >= 0 || root.current-item < root.model.length: Path { + stroke: MaterialPalette.accent-background; + 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.border-radius / 1px + ((root.border-radius - (root.split && root.current-item >= root.half-model-length ? root.inner-padding : root.outer-padding)) / 1px * cos(root.index-to-angle(root.current-item))); + y: root.border-radius / 1px + ((root.border-radius - (root.split && root.current-item >= root.half-model-length ? root.inner-padding : root.outer-padding)) / 1px * sin(root.index-to-angle(root.current-item))); + } + } + + Rectangle { + width: 8px; + height: 8px; + background: MaterialPalette.accent-background; + border-radius: 4px; + } + + if root.current-item >= 0 || root.current-item < root.model.length: Rectangle { + x: get-index-x(root.current-item); + y: get-index-y(root.current-item); + width: root.picker-ditameter; + height: root.picker-ditameter; + border-radius: root.picker-ditameter / 2; + background: MaterialPalette.accent-background; + } + + for val[index] in root.model: ValuePicker { + x: root.get-index-x(index); + y: root.get-index-y(index); + width: root.picker-ditameter; + height: root.picker-ditameter; + value: val; + selected: index == root.current-item; + + clicked => { + root.set-current-item(index); + } + } + + pure function index-to-angle(index: int) -> angle { + if root.split { + if index >= root.half-model-length { + return clamp((index - root.half-model-length) / root.half-model-length * 1turn, 0, 0.999999turn) - root.rotation; + } + return clamp(index / root.half-model-length * 1turn, 0, 0.99999turn) - root.rotation; + } + clamp(index / root.model.length * 1turn, 0, 0.99999turn) - root.rotation; + } + + pure function get-index-x(index: int) -> length { + if root.split && index >= root.half-model-length { + return root.center + (root.radius-inner / 1px * cos(root.index-to-angle(index))) * 1px; + } + root.center + (root.radius / 1px * cos(root.index-to-angle(index))) * 1px + } + + pure function get-index-y(index: int) -> length { + if root.split && index >= root.half-model-length { + return root.center + (root.radius-inner / 1px * sin(root.index-to-angle(index))) * 1px; + } + root.center + (root.radius / 1px * sin(root.index-to-angle(index))) * 1px + } + + function set-current-item(index: int) { + if root.current-item == index { + return; + } + root.current-item = index; + root.curren-item-changed(index); + } +} + +export component PeriodSelectorItem { + in property text <=> label.text; + in property checked; + + callback clicked <=> touch-area.clicked; + + touch-area := TouchArea { } + + background-layer := Rectangle { } + + label := Text { + font-size: MaterialFontSettings.body-large.font-size; + font-weight: MaterialFontSettings.body-large.font-weight; + horizontal-alignment: center; + } + + states [ + checked when root.checked: { + background-layer.background: MaterialPalette.tertiary-container; + } + ] +} + +export component PeriodSelector { + in property vertical; + in-out property am-selected: true; + + min-height: 38px; + + Rectangle { + border-radius: border.border-radius; + clip: true; + if root.vertical: VerticalLayout { + PeriodSelectorItem { + text: "AM"; + + checked: root.am-selected; + + clicked => { + root.am-selected = true; + } + } + + Rectangle { + height: 1px; + background: border.border-color; + vertical-stretch: 0; + } + + PeriodSelectorItem { + text: "PM"; + + checked: !root.am-selected; + + clicked => { + root.am-selected = false; + } + } + } + + if !root.vertical: HorizontalLayout { + PeriodSelectorItem { + text: "AM"; + + checked: root.am-selected; + + clicked => { + root.am-selected = true; + } + } + + Rectangle { + width: 1px; + background: border.border-color; + horizontal-stretch: 0; + } + + PeriodSelectorItem { + text: "PM"; + + checked: !root.am-selected; + + clicked => { + root.am-selected = false; + } + } + } + } + + border := Rectangle { + border-radius: 8px; + border-width: 1px; + border-color: MaterialPalette.border; + } +} + +export component TimePickerInput { + in property checked; + in-out property value; + in property read-only <=> text-input.read-only; + + callback clicked; + + min-width: max(96px, text-input.min-width); + min-height: max(80px, text-input.min-height); + vertical-stretch: 0; + horizontal-stretch: 0; + + background-layer := Rectangle { + border-radius: 8px; + background: MaterialPalette.surface-container-highest; + } + + text-input := TextInput { + vertical-alignment: center; + horizontal-alignment: center; + text: root.value; + width: 100%; + height: 100%; + color: MaterialPalette.foreground; + font-size: 57 * 0.0625rem; + font-weight: 400; + + accepted => { + if (!self.text.is-float()) { + self.text = root.value; + } + } + + edited => { + if (self.text.is-float() && self.text.to-float() != root.value) { + root.update-value(self.text.to-float()); + } else if (!self.text.is-float()) { + self.text = root.value; + } + } + } + + if root.read-only: TouchArea { + clicked => { + root.clicked(); + } + } + + function update-value(value: int) { + root.value = value; + text-input.text = root.value; + } + + states [ + checked when root.checked: { + background-layer.background: MaterialPalette.accent-container; + } + ] +} + +export enum TimePickerMode { + Select, + Enter +} + +export enum TimeMode { + Twelve, + TwentyFour +} + +export component TimePicker { + in property title: "Select time"; + in property mode; + in property time-mode; + + property <[int]> twelf-hour-model: [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + property <[int]> twenty-four-hour-model: [ + 12, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 24, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ]; + property <[int]> hour-model: root.time-mode == TimeMode.Twelve ? root.twelf-hour-model : root.twenty-four-hour-model; + property <[int]> minute-model: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]; + property hours-selected: true; + property current-hour; + property current-minute; + property current-hour-value: root.hour-model[root.current-hour]; + property current-minute-value: root.minute-model[root.current-minute]; + + callback cancel-clicked(); + callback ok-clicked; + min-width: layout.min-width; + min-height: layout.min-height; + + Rectangle { + background: MaterialPalette.surface-container-high; + border-radius: 28px; + } + + layout := VerticalBox { + Text { + horizontal-alignment: left; + overflow: elide; + text: root.title; + font-size: MaterialFontSettings.label-medium.font-size; + font-weight: MaterialFontSettings.label-medium.font-weight; + } + + HorizontalLayout { + VerticalLayout { + alignment: center; + spacing: 8px; + + HorizontalLayout { + spacing: 4px; + + TimePickerInput { + read-only: root.mode == TimePickerMode.Select; + checked: self.read-only && root.hours-selected; + value: root.current-hour-value; + + clicked => { + root.set-hours-selection(); + } + } + + separator := Text { + text: ":"; + color: MaterialPalette.foreground; + font-size: 57 * 0.0625rem; + font-weight: 400; + vertical-alignment: center; + } + + TimePickerInput { + read-only: root.mode == TimePickerMode.Select; + checked: self.read-only && !root.hours-selected; + value: root.current-minute-value; + + clicked => { + root.set-minute-selection(); + } + } + } + + if root.time-mode == TimeMode.Twelve: PeriodSelector { } + } + + Rectangle { } + + clock := Clock { + width: 256px; + height: 256px; + + model: root.hour-model; + + curren-item-changed(index) => { + root.time-selected(index); + } + } + } + + HorizontalLayout { + spacing: 8px; + + keyboard-button := IconButton { + icon: Icons.keyboard; + } + + Rectangle { } + + cancel-button := TextButton { + text: "Cancel"; + } + + ok-button := TextButton { + text: "OK"; + } + } + } + + // FIXME: refactor with states instead, twenty-four-hours, hours, mintues???? + + function set-minute-selection() { + root.hours-selected = false; + clock.split = false; + clock.model = root.minute-model; + clock.current-item = root.current-minute; + } + + function set-hours-selection() { + root.hours-selected = true; + clock.split = root.time-mode == TimeMode.TwentyFour; + clock.model = root.hour-model; + clock.current-item = root.current-hour; + } + + function time-selected(index: int) { + if root.hours-selected { + root.current-hour = index; + root.set-minute-selection(); + return; + } + root.current-minute = index; + } +} From 6a90157a818a2a279d6d38e56e5524949fbfaec5 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Thu, 2 May 2024 15:19:31 +0200 Subject: [PATCH 02/33] TimePicker: base styles and documentation --- .../src/language/widgets/time-picker.md | 50 ++ .../widgets/common/time-picker-base.slint | 471 +++++++++++ .../widgets/cosmic-base/time-picker.slint | 24 + .../widgets/cupertino-base/time-picker.slint | 0 .../widgets/fluent-base/time-picker.slint | 0 .../widgets/material-base/styling.slint | 1 + .../widgets/material-base/time-picker.slint | 778 ++++++++---------- 7 files changed, 878 insertions(+), 446 deletions(-) create mode 100644 docs/reference/src/language/widgets/time-picker.md create mode 100644 internal/compiler/widgets/common/time-picker-base.slint create mode 100644 internal/compiler/widgets/cosmic-base/time-picker.slint create mode 100644 internal/compiler/widgets/cupertino-base/time-picker.slint create mode 100644 internal/compiler/widgets/fluent-base/time-picker.slint diff --git a/docs/reference/src/language/widgets/time-picker.md b/docs/reference/src/language/widgets/time-picker.md new file mode 100644 index 00000000000..6691f47910d --- /dev/null +++ b/docs/reference/src/language/widgets/time-picker.md @@ -0,0 +1,50 @@ + + +## struct `Time` + +Defines a time with hour minutes and seconds. + +### Fields + +- **`hour`(int)**: The hour value. +- **`minute`(int)**: The minute value. +- **`second`(int)**: The second value. +## `TimePicker` + +A timer picker that is usd for selecting the time, in either 24-hour or AM/PM mode. + +### Properties + +- **`title`** (_in_ _string_): The text that is displayed at the top of the picker. +- **`ok-label`** (_in_ _string_): The text written in the ok button. +- **`cancel-label`** (_in_ _string_): The text written in the cancel button. +- **`is-twenty-four-hour`**: (_in_ _bool_): Sets to true to enable 24 hour selection otherwise it is displayed in AM/PM mode. +- **`curren-time`**: (_in-out_ _Time_): Gets and sets the current selected time.. + +### Callbacks + +- **`cancled()`**: The cancel button was clicked. +- **`accepted(Time)`** The ok button was clicked. + +### Example + +```slint +import { TimePicker } from "std-widgets.slint"; +export component Example inherits Window { + width: 200px; + height: 130px; + + time-picker-popup := Popup { + TimePicker { + + } + } + + Button { + text: "Open time picker"; + clicked => { + time-picker-popup.show(); + } + } +} +``` diff --git a/internal/compiler/widgets/common/time-picker-base.slint b/internal/compiler/widgets/common/time-picker-base.slint new file mode 100644 index 00000000000..7e010cbd239 --- /dev/null +++ b/internal/compiler/widgets/common/time-picker-base.slint @@ -0,0 +1,471 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.2 OR LicenseRef-Slint-commercial + +export struct TimeSelectorStyle { + foreground: brush, + foreground-selected: brush, + font-size: length, + font-weight: float} + +component TimeSelector inherits Rectangle { + in property selected; + in property value; + in property style; + callback clicked <=> touch-area.clicked; + + width: max(48px, label.min-width); + height: max(48px, label.min-height); + border-radius: max(root.width, root.height) / 2; + vertical-stretch: 0; + horizontal-stretch: 0; + + touch-area := TouchArea { } + + label := Text { + text: root.value; + vertical-alignment: center; + horizontal-alignment: center; + color: root.style.foreground; + font-size: root.style.font-size; + font-weight: root.style.font-weight; + } + + states [ + selected when root.selected: { + label.color: root.style.foreground-selected; + } + ] +} + +export struct ClockStyle { + background: brush, + foreground: brush, + time-selector-style: TimeSelectorStyle} + +export component Clock { + in property <[int]> model; + in property two-columns; + in property style; + in-out property current-item; + out property current-value: root.model[root.current-item]; + + callback curren-item-changed(/* index */ int); + + property radius: max(root.width, root.height) / 2; + property picker-ditameter: 48px; + 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-model-length: root.model.length / 2; + property rotation: 0.25turn; + + min-width: 256px; + min-height: 256px; + vertical-stretch: 0; + horizontal-stretch: 0; + + background-layer := Rectangle { + border-radius: max(self.width, self.height) / 2; + background: root.style.background; + } + + if root.current-item >= 0 || root.current-item < root.model.length: Path { + stroke: root.style.foreground; + 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.radius / 1px + ((root.radius - (root.two-columns && root.current-item >= root.half-model-length ? root.inner-padding : root.outer-padding)) / 1px * cos(root.index-to-angle(root.current-item))); + y: root.radius / 1px + ((root.radius - (root.two-columns && root.current-item >= root.half-model-length ? root.inner-padding : root.outer-padding)) / 1px * sin(root.index-to-angle(root.current-item))); + } + } + + Rectangle { + width: 8px; + height: 8px; + background: root.style.foreground; + border-radius: 4px; + } + + if root.current-item >= 0 || root.current-item < root.model.length: Rectangle { + x: get-index-x(root.current-item); + y: get-index-y(root.current-item); + width: root.picker-ditameter; + height: root.picker-ditameter; + border-radius: root.picker-ditameter / 2; + background: root.style.foreground; + } + + for val[index] in root.model: TimeSelector { + x: root.get-index-x(index); + y: root.get-index-y(index); + width: root.picker-ditameter; + height: root.picker-ditameter; + value: val; + selected: index == root.current-item; + style: root.style.time-selector-style; + + clicked => { + root.set-current-item(index); + } + } + + pure function index-to-angle(index: int) -> angle { + if root.two-columns { + if index >= root.half-model-length { + return clamp((index - root.half-model-length) / root.half-model-length * 1turn, 0, 0.999999turn) - root.rotation; + } + return clamp(index / root.half-model-length * 1turn, 0, 0.99999turn) - root.rotation; + } + clamp(index / root.model.length * 1turn, 0, 0.99999turn) - root.rotation; + } + + pure function get-index-x(index: int) -> length { + if root.two-columns && index >= root.half-model-length { + return root.center + (root.radius-inner / 1px * cos(root.index-to-angle(index))) * 1px; + } + root.center + (root.radius-outer / 1px * cos(root.index-to-angle(index))) * 1px + } + + pure function get-index-y(index: int) -> length { + if root.two-columns && index >= root.half-model-length { + return root.center + (root.radius-inner / 1px * sin(root.index-to-angle(index))) * 1px; + } + root.center + (root.radius-outer / 1px * sin(root.index-to-angle(index))) * 1px + } + + function set-current-item(index: int) { + if root.current-item == index { + return; + } + root.current-item = index; + root.curren-item-changed(index); + } +} + +export struct TimePickerInputStyle { + background: brush, + background-selected: brush, + foreground: brush, + border-radius: length, + font-size: length, + font-weight: float} + +export component TimePickerInput { + in property style; + in property read-only <=> text-input.read-only; + in property checked; + in-out property value; + + callback clicked; + callback accepted(int); + + min-width: max(96px, text-input.min-width); + min-height: max(80px, text-input.min-height); + vertical-stretch: 0; + horizontal-stretch: 0; + + background-layer := Rectangle { + border-radius: root.style.border-radius; + background: root.style.background; + } + + text-input := TextInput { + vertical-alignment: center; + horizontal-alignment: center; + text: root.value; + width: 100%; + height: 100%; + color: root.style.foreground; + font-size: root.style.font-size; + font-weight: root.style.font-weight; + input-type: number; + + accepted => { + if !self.text.is-float() { + return; + } + root.accepted(self.text.to-float()); + } + } + + if root.read-only: TouchArea { + clicked => { + root.clicked(); + } + } + + function update-value(value: int) { + root.value = value; + text-input.text = root.value; + } + + states [ + checked when root.checked: { + background-layer.background: root.style.background-selected; + } + ] +} + +export struct PeriodSelectorItemStyle { + font-size: length, + font-weight: float, + foreground: brush, + background-selected: brush, + foreground-selected: brush} + +export component PeriodSelectorItem { + in property style; + in property text <=> label.text; + in property checked; + + callback clicked <=> touch-area.clicked; + + touch-area := TouchArea { } + + background-layer := Rectangle { } + + label := Text { + font-size: root.style.font-size; + font-weight: root.style.font-weight; + color: root.style.foreground; + horizontal-alignment: center; + } + + states [ + checked when root.checked: { + background-layer.background: root.style.background-selected; + label.color: root.style.foreground; + } + ] +} + +export struct PeriodSelectorStyle { + item-style: PeriodSelectorItemStyle, + border-brush: brush} + +export component PeriodSelector { + in property style; + in property am-selected; + + callback update-period(bool); + + min-height: 38px; + + Rectangle { + border-radius: border.border-radius; + clip: true; + + VerticalLayout { + PeriodSelectorItem { + text: "AM"; + checked: root.am-selected; + style: root.style.item-style; + + 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; + style: root.style.item-style; + + clicked => { + if !root.am-selected { + return; + } + root.update-period(false); + } + } + } + } + + border := Rectangle { + border-radius: 8px; + border-width: 1px; + border-color: root.style.border-brush; + } +} + +export struct Time { + hour: int, + minute: int} + +export struct TimePickerStyle { + foreground: brush, + spacing: length, + clock-style: ClockStyle, + input-style: TimePickerInputStyle, + period-selector-style: PeriodSelectorStyle, +} + +export component TimePickerBase { + in property twenty-four-hour; + in property selection-mode: true; + in property style; + in-out property