mirror of
https://github.com/slint-ui/slint.git
synced 2025-07-16 01:25:27 +00:00

The ones that have a comment already. Tests that the name is consistent accross the styles.
566 lines
16 KiB
Text
566 lines
16 KiB
Text
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
import { ColoredTextStyle } from "./internal-components.slint";
|
|
|
|
export struct TimeSelectorStyle {
|
|
foreground: brush,
|
|
foreground-selected: brush,
|
|
font-size: length,
|
|
font-weight: float
|
|
}
|
|
|
|
component TimeSelector inherits Rectangle {
|
|
in property <bool> selected;
|
|
in property <int> value;
|
|
in property <TimeSelectorStyle> 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 <bool> two-columns;
|
|
in property <ClockStyle> style;
|
|
in property <int> total;
|
|
in-out property <int> current-item;
|
|
in property <int> current-value;
|
|
|
|
callback current-item-changed(index: int);
|
|
|
|
property <length> radius: max(root.width, root.height) / 2;
|
|
property <length> picker-ditameter: 48px;
|
|
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: 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) {
|
|
root.current-item-changed(index);
|
|
}
|
|
}
|
|
|
|
export struct TimePickerInputStyle {
|
|
background: brush,
|
|
background-selected: brush,
|
|
foreground: brush,
|
|
foreground-selected: brush,
|
|
border-radius: length,
|
|
font-size: length,
|
|
font-weight: float
|
|
}
|
|
|
|
export component TimePickerInput {
|
|
in property <TimePickerInputStyle> style;
|
|
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(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;
|
|
text-input.color: root.style.foreground-selected;
|
|
}
|
|
]
|
|
}
|
|
|
|
export struct PeriodSelectorItemStyle {
|
|
font-size: length,
|
|
font-weight: float,
|
|
foreground: brush,
|
|
background-selected: brush,
|
|
foreground-selected: brush
|
|
}
|
|
|
|
export component PeriodSelectorItem {
|
|
in property <PeriodSelectorItemStyle> style;
|
|
in property <string> text <=> label.text;
|
|
in property <bool> 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 <PeriodSelectorStyle> style;
|
|
in property <bool> 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: ColoredTextStyle,
|
|
}
|
|
|
|
export component TimePickerBase {
|
|
in property <bool> use-24-hour-format: SlintInternal.use-24-hour-format;
|
|
in property <bool> selection-mode: true;
|
|
in property <TimePickerStyle> style;
|
|
in property <Time> time: { hour: 12 };
|
|
in property <string> title;
|
|
|
|
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-item: 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();
|
|
|
|
min-width: content-layer.min-width;
|
|
min-height: content-layer.min-height;
|
|
|
|
content-layer := VerticalLayout {
|
|
spacing: root.style.vertical-spacing;
|
|
|
|
HorizontalLayout {
|
|
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;
|
|
}
|
|
}
|
|
|
|
HorizontalLayout {
|
|
spacing: root.style.horizontal-spacing;
|
|
alignment: center;
|
|
|
|
hour-time-picker := TimePickerInput {
|
|
read-only: root.selection-mode;
|
|
checked: self.read-only && !root.minutes-selected;
|
|
text: root.current-time.hour;
|
|
style: root.style.input-style;
|
|
|
|
accessible-role: AccessibleRole.text-input;
|
|
accessible-value: self.text;
|
|
accessible-label: "hour";
|
|
|
|
clicked => {
|
|
root.minutes-selected = false;
|
|
}
|
|
}
|
|
|
|
separator := Text {
|
|
text: ":";
|
|
color: root.style.foreground;
|
|
font-size: root.style.input-style.font-size;
|
|
font-weight: root.style.input-style.font-weight;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
minute-time-picker := TimePickerInput {
|
|
read-only: root.selection-mode;
|
|
checked: self.read-only && root.minutes-selected;
|
|
text: root.current-time.minute;
|
|
style: root.style.input-style;
|
|
|
|
accessible-role: AccessibleRole.text-input;
|
|
accessible-value: self.text;
|
|
accessible-label: "minute";
|
|
|
|
clicked => {
|
|
root.minutes-selected = true;
|
|
}
|
|
}
|
|
|
|
if !root.use-24-hour-format: PeriodSelector {
|
|
am-selected: root.am-selected;
|
|
style: root.style.period-selector-style;
|
|
|
|
update-period(am) => {
|
|
root.am-selected = am;
|
|
}
|
|
}
|
|
}
|
|
|
|
if root.selection-mode: HorizontalLayout {
|
|
alignment: center;
|
|
|
|
Clock {
|
|
width: 256px;
|
|
height: 256px;
|
|
model: root.current-model;
|
|
style: root.style.clock-style;
|
|
two-columns: !root.minutes-selected && root.use-24-hour-format;
|
|
current-item: root.current-item;
|
|
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-item-changed(index) => {
|
|
root.update-time-by-item(index);
|
|
|
|
if !root.minutes-selected {
|
|
root.minutes-selected = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pure public function ok-enabled() -> bool {
|
|
if root.selection-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.selection-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 selection-mode => {
|
|
if !root.selection-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;
|
|
}
|
|
return 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;
|
|
}
|
|
}
|