From 3ccee816bec9cd7664e2c210511c98adb3bcf656 Mon Sep 17 00:00:00 2001 From: Florian Blasius Date: Wed, 5 Jun 2024 13:23:06 +0000 Subject: [PATCH] Added TimePicker widget (#5251) --- .reuse/dep5 | 4 + CHANGELOG.md | 1 + api/cpp/include/slint.h | 5 + api/rs/slint/private_unstable_api.rs | 4 + docs/reference/src/language/widgets/index.rst | 1 + .../src/language/widgets/timepicker.md | 63 ++ examples/gallery/ui/pages/controls_page.slint | 31 +- internal/compiler/expression_tree.rs | 6 + internal/compiler/generator/cpp.rs | 3 + internal/compiler/generator/rust.rs | 3 + .../llr/optim_passes/inline_expressions.rs | 1 + internal/compiler/lookup.rs | 38 +- .../widgets/common/internal-components.slint | 8 + .../widgets/common/time-picker-base.slint | 570 ++++++++++++++++++ .../compiler/widgets/cosmic-base/_clock.svg | 12 + .../widgets/cosmic-base/_keyboard.svg | 11 + .../cosmic-base/std-widgets-base.slint | 3 + .../widgets/cosmic-base/styling.slint | 2 + .../widgets/cosmic-base/time-picker.slint | 108 ++++ .../widgets/cupertino-base/_clock.svg | 3 + .../widgets/cupertino-base/_keyboard.svg | 1 + .../cupertino-base/std-widgets-base.slint | 3 + .../widgets/cupertino-base/styling.slint | 2 + .../widgets/cupertino-base/time-picker.slint | 107 ++++ .../compiler/widgets/fluent-base/_clock.svg | 3 + .../widgets/fluent-base/_keyboard.svg | 1 + .../fluent-base/std-widgets-base.slint | 3 + .../widgets/fluent-base/styling.slint | 2 + .../widgets/fluent-base/time-picker.slint | 106 ++++ .../compiler/widgets/material-base/_clock.svg | 3 + .../widgets/material-base/_keyboard.svg | 1 + .../widgets/material-base/button.slint | 192 ++++-- .../material-base/std-widgets-base.slint | 3 + .../widgets/material-base/styling.slint | 34 +- .../widgets/material-base/time-picker.slint | 105 ++++ internal/compiler/widgets/qt/_clock.svg | 12 + internal/compiler/widgets/qt/_keyboard.svg | 11 + .../compiler/widgets/qt/std-widgets.slint | 3 + internal/compiler/widgets/qt/styling.slint | 7 + .../compiler/widgets/qt/time-picker.slint | 109 ++++ internal/core/date_time.rs | 17 + internal/core/lib.rs | 1 + internal/interpreter/eval.rs | 1 + tests/cases/widgets/timepicker.slint | 90 +++ 44 files changed, 1612 insertions(+), 82 deletions(-) create mode 100644 docs/reference/src/language/widgets/timepicker.md create mode 100644 internal/compiler/widgets/common/internal-components.slint create mode 100644 internal/compiler/widgets/common/time-picker-base.slint create mode 100644 internal/compiler/widgets/cosmic-base/_clock.svg create mode 100644 internal/compiler/widgets/cosmic-base/_keyboard.svg create mode 100644 internal/compiler/widgets/cosmic-base/time-picker.slint create mode 100644 internal/compiler/widgets/cupertino-base/_clock.svg create mode 100644 internal/compiler/widgets/cupertino-base/_keyboard.svg create mode 100644 internal/compiler/widgets/cupertino-base/time-picker.slint create mode 100644 internal/compiler/widgets/fluent-base/_clock.svg create mode 100644 internal/compiler/widgets/fluent-base/_keyboard.svg create mode 100644 internal/compiler/widgets/fluent-base/time-picker.slint create mode 100644 internal/compiler/widgets/material-base/_clock.svg create mode 100644 internal/compiler/widgets/material-base/_keyboard.svg create mode 100644 internal/compiler/widgets/material-base/time-picker.slint create mode 100644 internal/compiler/widgets/qt/_clock.svg create mode 100644 internal/compiler/widgets/qt/_keyboard.svg create mode 100644 internal/compiler/widgets/qt/styling.slint create mode 100644 internal/compiler/widgets/qt/time-picker.slint create mode 100644 internal/core/date_time.rs create mode 100644 tests/cases/widgets/timepicker.slint diff --git a/.reuse/dep5 b/.reuse/dep5 index 20bf545803b..3c2763638cd 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -104,6 +104,10 @@ Files: internal/compiler/widgets/cosmic-base/_*.svg Copyright: "Cosmic Icons" by System76 License: CC-BY-SA-4.0 +Files: internal/compiler/widgets/qt/_*.svg +Copyright: "Cosmic Icons" by System76 +License: CC-BY-SA-4.0 + Files: internal/backends/linuxkms/mouse-pointer.svg examples/virtual_keyboard/ui/assets/*.svg examples/ffmpeg/pause.svg examples/ffmpeg/play.svg examples/gstreamer-player/*.svg examples/uefi-demo/resource/cursor.png Copyright: Copyright © 2018 Dave Gandy & Fork Awesome License: MIT diff --git a/CHANGELOG.md b/CHANGELOG.md index 3169b31835c..0fadd9a2c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project are documented in this file. - Fixed set current-index of ComboBox to -1 does not reset current-value - Fixed issue where the text of `SpinBox` is not updated after value is changed from outside - Added `step-size` to `SpinBox` + - Added `TimePicker` widget. ## [1.6.0] - 2024-05-13 diff --git a/api/cpp/include/slint.h b/api/cpp/include/slint.h index b21dd7d2924..b24f29d2dcd 100644 --- a/api/cpp/include/slint.h +++ b/api/cpp/include/slint.h @@ -1266,6 +1266,11 @@ inline SharedString translate(const SharedString &original, const SharedString & return result; } +inline bool use_24_hour_format() +{ + return cbindgen_private::slint_use_24_hour_format(); +} + } // namespace private_api #ifdef SLINT_FEATURE_GETTEXT diff --git a/api/rs/slint/private_unstable_api.rs b/api/rs/slint/private_unstable_api.rs index 7a65081cfdd..f9d06b114f0 100644 --- a/api/rs/slint/private_unstable_api.rs +++ b/api/rs/slint/private_unstable_api.rs @@ -165,6 +165,10 @@ pub fn init_translations(domain: &str, dirname: impl Into) { i_slint_core::translations::gettext_bindtextdomain(domain, dirname.into()).unwrap() } +pub fn use_24_hour_format() -> bool { + i_slint_core::date_time::use_24_hour_format() +} + /// internal re_exports used by the macro generated pub mod re_exports { pub use alloc::boxed::Box; diff --git a/docs/reference/src/language/widgets/index.rst b/docs/reference/src/language/widgets/index.rst index 0e120ed9c54..5318c8c8af1 100644 --- a/docs/reference/src/language/widgets/index.rst +++ b/docs/reference/src/language/widgets/index.rst @@ -33,3 +33,4 @@ Widgets tabwidget.md textedit.md verticalbox.md + timepicker.md diff --git a/docs/reference/src/language/widgets/timepicker.md b/docs/reference/src/language/widgets/timepicker.md new file mode 100644 index 00000000000..230752504ce --- /dev/null +++ b/docs/reference/src/language/widgets/timepicker.md @@ -0,0 +1,63 @@ + + +## struct `Time` + +Defines a time with hours, minutes, and seconds. + +### Fields + +- **`hour`(int)**: The hour value (range from 0 to 23). +- **`minute`(int)**: The minute value (range from 1 to 59). +- **`second`(int)**: The second value (range form 1 to 59). + +## `TimePicker` + +Use the timer picker to select the time, in either 24-hour or 12-hour mode (AM/PM). + +### Properties + +- **`use-24-hour-format`**: (_in_ _bool_): If set to `true` 24 hours are displayed otherwise it is displayed in AM/PM mode. (default: system default, if cannot be determined then `true`) +- **`title`** (_in_ _string_): The text that is displayed at the top of the picker. +- **`cancel-label`** (_in_ _string_): The text written in the cancel button. +- **`ok-label`** (_in_ _string_): The text written in the ok button. +- **`time`**: (_in_ _Time_): Set the initial displayed time. + +### Callbacks + +- **`canceled()`**: The cancel button was clicked. +- **`accepted(Time)`** The ok button was clicked. + +### Example + +```slint +import { TimePicker, Button } from "std-widgets.slint"; +export component Example inherits Window { + width: 600px; + height: 600px; + + time-picker-button := Button { + text: @tr("Open TimePicker"); + + clicked => { + time-picker.show(); + } + } + + time-picker := PopupWindow { + width: 340px; + height: 500px; + close-on-click: false; + + TimePicker { + canceled => { + time-picker.close(); + } + + accepted(time) => { + debug(time); + time-picker.close(); + } + } + } +} +``` diff --git a/examples/gallery/ui/pages/controls_page.slint b/examples/gallery/ui/pages/controls_page.slint index 268da5d2f23..b76ac1499c7 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"; @@ -87,7 +87,7 @@ export component ControlsPage inherits Page { } GroupBox { - title: @tr("LineEdit - SpinBox"); + title: @tr("LineEdit - SpinBox - TimePicker"); vertical-stretch: 0; HorizontalBox { @@ -104,6 +104,14 @@ export component ControlsPage inherits Page { value: 42; enabled: GallerySettings.widgets-enabled; } + + time-picker-button := Button { + text: @tr("Open TimePicker"); + + clicked => { + time-picker.show(); + } + } } } @@ -139,6 +147,7 @@ export component ControlsPage inherits Page { row: 0; col: 1; rowspan: 2; + Spinner { progress: i-progress-indicator.progress; indeterminate: i-progress-indicator.indeterminate; @@ -211,4 +220,22 @@ export component ControlsPage inherits Page { } } } + + time-picker := PopupWindow { + x: (root.width - 340px) / 2; + y: (root.height - 500px) / 2; + width: 340px; + height: 500px; + close-on-click: false; + + TimePicker { + canceled => { + time-picker.close(); + } + + accepted(time) => { + time-picker.close(); + } + } + } } diff --git a/internal/compiler/expression_tree.rs b/internal/compiler/expression_tree.rs index 0e5afa25520..a3e225dfd7b 100644 --- a/internal/compiler/expression_tree.rs +++ b/internal/compiler/expression_tree.rs @@ -68,6 +68,7 @@ pub enum BuiltinFunction { RegisterCustomFontByMemory, RegisterBitmapFont, Translate, + Use24HourFormat, } #[derive(Debug, Clone)] @@ -273,6 +274,9 @@ impl BuiltinFunction { Type::Array(Type::String.into()), ], }, + BuiltinFunction::Use24HourFormat => { + Type::Function { return_type: Box::new(Type::Bool), args: vec![] } + } } } @@ -330,6 +334,7 @@ impl BuiltinFunction { | BuiltinFunction::RegisterCustomFontByMemory | BuiltinFunction::RegisterBitmapFont => false, BuiltinFunction::Translate => false, + BuiltinFunction::Use24HourFormat => false, } } @@ -380,6 +385,7 @@ impl BuiltinFunction { | BuiltinFunction::RegisterCustomFontByMemory | BuiltinFunction::RegisterBitmapFont => false, BuiltinFunction::Translate => true, + BuiltinFunction::Use24HourFormat => true, } } } diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index d4733a66309..483533ac63f 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -3355,6 +3355,9 @@ fn compile_builtin_function_call( BuiltinFunction::Translate => { format!("slint::private_api::translate({})", a.join(",")) } + BuiltinFunction::Use24HourFormat => { + format!("slint::private_api::use_24_hour_format()") + } } } diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 77fe79c2ad8..02d28f7d75f 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -2698,6 +2698,9 @@ fn compile_builtin_function_call( BuiltinFunction::Translate => { quote!(slint::private_unstable_api::translate(#((#a) as _),*)) } + BuiltinFunction::Use24HourFormat => { + quote!(slint::private_unstable_api::use_24_hour_format()) + } BuiltinFunction::ItemAbsolutePosition => { if let [Expression::PropertyReference(pr)] = arguments { let item_rc = access_item_rc(pr, ctx); diff --git a/internal/compiler/llr/optim_passes/inline_expressions.rs b/internal/compiler/llr/optim_passes/inline_expressions.rs index 4be46e8dce9..2ca9b1882f6 100644 --- a/internal/compiler/llr/optim_passes/inline_expressions.rs +++ b/internal/compiler/llr/optim_passes/inline_expressions.rs @@ -109,6 +109,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize { BuiltinFunction::SetTextInputFocused => PROPERTY_ACCESS_COST, BuiltinFunction::TextInputFocused => PROPERTY_ACCESS_COST, BuiltinFunction::Translate => 2 * ALLOC_COST + PROPERTY_ACCESS_COST, + BuiltinFunction::Use24HourFormat => 2 * ALLOC_COST + PROPERTY_ACCESS_COST, } } diff --git a/internal/compiler/lookup.rs b/internal/compiler/lookup.rs index 8209776e0d6..7cb6c4f02c6 100644 --- a/internal/compiler/lookup.rs +++ b/internal/compiler/lookup.rs @@ -814,16 +814,36 @@ impl LookupObject for SlintInternal { ctx: &LookupCtx, f: &mut impl FnMut(&str, LookupResult) -> Option, ) -> Option { - f( - "color-scheme", - Expression::FunctionCall { - function: Expression::BuiltinFunctionReference(BuiltinFunction::ColorScheme, None) + None.or_else(|| { + f( + "color-scheme", + Expression::FunctionCall { + function: Expression::BuiltinFunctionReference( + BuiltinFunction::ColorScheme, + None, + ) .into(), - arguments: vec![], - source_location: ctx.current_token.as_ref().map(|t| t.to_source_location()), - } - .into(), - ) + arguments: vec![], + source_location: ctx.current_token.as_ref().map(|t| t.to_source_location()), + } + .into(), + ) + .or_else(|| { + f( + "use-24-hour-format", + Expression::FunctionCall { + function: Expression::BuiltinFunctionReference( + BuiltinFunction::Use24HourFormat, + None, + ) + .into(), + arguments: vec![], + source_location: ctx.current_token.as_ref().map(|t| t.to_source_location()), + } + .into(), + ) + }) + }) } } diff --git a/internal/compiler/widgets/common/internal-components.slint b/internal/compiler/widgets/common/internal-components.slint new file mode 100644 index 00000000000..6e71cb52c14 --- /dev/null +++ b/internal/compiler/widgets/common/internal-components.slint @@ -0,0 +1,8 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +export struct TextStyle { + font-size: length, + font-weight: int, + foreground: brush, +} 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..5cf225e13e0 --- /dev/null +++ b/internal/compiler/widgets/common/time-picker-base.slint @@ -0,0 +1,570 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +import { TextStyle } from "./internal-components.slint"; + +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, text-label.min-width); + height: max(48px, text-label.min-height); + border-radius: max(root.width, root.height) / 2; + vertical-stretch: 0; + horizontal-stretch: 0; + + touch-area := TouchArea { } + + text-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: { + text-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 property total; + in-out property current-item; + in property current-value; + + 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-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: 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.current-x + root.picker-ditameter / 2) / 1px; + y: (root.current-y + root.picker-ditameter / 2) / 1px; + } + } + + Rectangle { + width: 8px; + height: 8px; + background: root.style.foreground; + border-radius: 4px; + } + + if root.current-item < 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: root.style.foreground; + + if root.current-item < 0: Rectangle { + width: 4px; + height: 4px; + border-radius: 2px; + background: root.style.time-selector-style.foreground; + } + } + + 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-item; + style: root.style.time-selector-style; + accessible-role: button; + accessible-label: @tr("{} Hours or minutes of {}", val, root.total); + accessible-action-default => { + self.clicked(); + } + + clicked => { + root.set-current-item(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-item(index: int) { + if root.current-item == index { + return; + } + 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 text <=> text-input.text; + + callback clicked; + callback edited(int); + + min-width: max(96px, text-input.min-width); + min-height: max(80px, text-input.min-height); + vertical-stretch: 0; + horizontal-stretch: 0; + + forward-focus: text-input; + + background-layer := Rectangle { + border-radius: root.style.border-radius; + background: root.style.background; + } + + text-input := TextInput { + vertical-alignment: center; + horizontal-alignment: center; + width: 100%; + height: 100%; + color: root.style.foreground; + font-size: root.style.font-size; + font-weight: root.style.font-weight; + input-type: number; + edited => { + root.edited(self.text.to-float()); + } + } + + if root.read-only: TouchArea { + clicked => { + root.clicked(); + } + } + + 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-selected; + } + ] +} + +export struct PeriodSelectorStyle { + item-style: PeriodSelectorItemStyle, + border-brush: brush, + border-radius: length, + border-width: length} + +export component PeriodSelector { + in property style; + in property am-selected; + + callback update-period(bool); + + min-width: max(38px, layout.min-width); + accessible-label: "AM or PM"; + accessible-role: checkbox; + accessible-checked: root.am-selected; + + Rectangle { + border-radius: border.border-radius; + clip: true; + + layout := 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: root.style.border-radius; + border-width: root.style.border-width; + border-color: root.style.border-brush; + } +} + +export struct Time { + hour: int, + minute: int, + second: int +} + +export struct TimePickerStyle { + foreground: brush, + horizontal-spacing: length, + vertical-spacing: length, + clock-style: ClockStyle, + input-style: TimePickerInputStyle, + period-selector-style: PeriodSelectorStyle, + title-style: TextStyle, +} + +export component TimePickerBase { + in property use-24-hour-format: SlintInternal.use-24-hour-format; + in property selection-mode: true; + in property style; + in property