// 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; rotation-angle: root.checked ? 180deg : 0; rotation-origin-x: self.width / 2; rotation-origin-y: self.height / 2; animate rotation-angle { duration: 250ms; } } } states [ disabled when !root.enabled : { root.opacity: 0.38; } ] } export component DatePickerPopup inherits PopupWindow { in property 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; } }