// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 import { ScrollView, LineEdit } from "std-widgets-impl.slint"; import { StateLayer, IconButton, IconButtonStyle, SelectionButton, SelectionButtonStyle, ColoredTextStyle } from "./internal-components.slint"; export struct Date { year: int, month: int, day: int, } export struct CalendarDelegateStyle { font-size: length, font-weight: float, foreground: brush, state-brush: brush, background-selected: brush, foreground-selected: brush, state-brush-selected: brush, border-color-today: brush, foreground-today: brush, state-brush-today: brush, } component CalendarHeaderDelegate { in property text <=> text-label.text; in property font-size; in property font-weight; in property foreground; min-width: max(40px, content-layer.min-width); min-height: max(40px, content-layer.min-height); content-layer := VerticalLayout { text-label := Text { vertical-alignment: center; horizontal-alignment: center; font-size: root.font-size; font-weight: root.font-weight; color: root.foreground; } } } component CalendarDelegate { in property selected; in property today; in property text <=> text-label.text; in property style; in property enabled: true; callback clicked <=> touch-area.clicked; min-width: max(40px, content-layer.min-width); min-height: max(40px, 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 := Text { vertical-alignment: center; horizontal-alignment: center; color: root.style.foreground; font-size: root.style.font-size; font-weight: root.style.font-weight; } } 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; state-brush: root.style.state-brush; } states [ disabled when !root.enabled : { root.opacity: 0.38; } selected when root.selected : { background-layer.background: root.style.background-selected; text-label.color: root.style.foreground-selected; state-layer.state-brush: root.style.state-brush-selected; } today when root.today : { background-layer.border-color: root.style.border-color-today; background-layer.border-width: 1px; state-layer.state-brush: root.style.state-brush-today; } ] } export struct CalendarStyle { delegate-style: CalendarDelegateStyle, spacing: length, } export component Calendar { in property column-count; in property row-count; in property delegate-size; in property style; 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; font-size: root.style.delegate-style.font-size; font-weight: root.style.delegate-style.font-weight; foreground: root.style.delegate-style.foreground; } // 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; style: root.style.delegate-style; 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) * root.style.spacing } function delegate-y(index: int) -> length { root.row-for-index(index) * root.delegate-size + root.row-for-index(index) * 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 delegate-style; 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; style: root.delegate-style; 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 struct DatePickerStyle { border-brush: brush, horizontal-spacing: length, vertical-spacing: length, calendar-style: CalendarStyle, icon-button-style: IconButtonStyle, selection-button-style: SelectionButtonStyle, current-day-style: ColoredTextStyle, title-style: ColoredTextStyle, next-icon: image, previous-icon: image, drop-down-icon: image, input-icon: image, calendar-icon: image, } export component DatePickerBase { in property date : { day: today[0], month: today[1], year: today[2] }; in property style; in property title; in property input-title: @tr("Enter date"); in property input-placeholder-text: "mm/dd/yyyy"; in property input-format: "%m/%d/%Y"; // this is used for the navigation between months 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) * root.style.calendar-style.spacing; property calendar-min-height: root.delegate-size *(root.calendar-row-count + 1) + (root.calendar-row-count - 1) * root.style.calendar-style.spacing; 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: SlintInternal.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: SlintInternal.date_now(); property start-column: SlintInternal.month_offset(root.display-date.month, root.display-date.year); property current-month: SlintInternal.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: SlintInternal.format_date("%a, %b %d", root.current-date.day, root.current-date.month, root.current-date.year); property <[int]> input-formatted: SlintInternal.parse_date(root.current-input, root.input-format); min-width: content-layer.min-width; min-height: content-layer.min-height; content-layer := VerticalLayout { spacing: root.style.vertical-spacing; Text { text: root.title; horizontal-alignment: left; overflow: elide; font-size: root.style.title-style.font-size; font-weight: root.style.title-style.font-weight; color: root.style.title-style.foreground; } header := HorizontalLayout { spacing: root.style.horizontal-spacing; Text { text: root.selection-mode ? root.current-day : root.input-title; horizontal-alignment: left; vertical-alignment: center; font-size: root.style.current-day-style.font-size; font-weight: root.style.current-day-style.font-weight; color: root.style.current-day-style.foreground; } IconButton { icon: root.selection-mode ? root.style.input-icon : root.style.calendar-icon; style: root.style.icon-button-style; accessible-label: "Toggle selection mode"; clicked => { root.toggle-selection-mode(); } } } Rectangle { height: 1px; background: root.style.border-brush; } if root.selection-mode : HorizontalLayout { spacing: root.style.horizontal-spacing; VerticalLayout { horizontal-stretch: 0; alignment: center; SelectionButton { text: root.current-month; style: root.style.selection-button-style; icon: root.style.drop-down-icon; checked <=> root.year-selection; } } Rectangle {} IconButton { icon: root.style.previous-icon; style: root.style.icon-button-style; accessible-label: "Previous month"; clicked => { root.show-previous(); } } IconButton { icon: root.style.next-icon; style: root.style.icon-button-style; accessible-label: "Next month"; clicked => { root.show-next(); } } } if root.selection-mode : VerticalLayout { spacing: root.style.vertical-spacing; 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; style: root.style.calendar-style; 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; spacing: root.style.calendar-style.spacing; 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; delegate-style: root.style.calendar-style.delegate-style; selected-year: root.selected-year; today-year: root.today-year; select-year(year) => { root.select-year(year); } } } Rectangle { height: 1px; background: root.style.border-brush; visible: root.year-selection; } if !root.selection-mode : LineEdit { text <=> root.current-input; placeholder-text: root.input-placeholder-text; } } 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 { SlintInternal.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; } }