slint/ui-libraries/material/ui/components/time_picker.slint

674 lines
21 KiB
Text

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