1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4import { ColoredTextStyle } from "./internal-components.slint";
5
6export struct TimeSelectorStyle {
7 foreground: brush,
8 foreground-selected: brush,
9 font-size: length,
10 font-weight: float
11}
12
13component TimeSelector inherits Rectangle {
14 in property <bool> selected;
15 in property <int> value;
16 in property <TimeSelectorStyle> style;
17 callback clicked <=> touch-area.clicked;
18
19 width: max(48px, text-label.min-width);
20 height: max(48px, text-label.min-height);
21 border-radius: max(root.width, root.height) / 2;
22 vertical-stretch: 0;
23 horizontal-stretch: 0;
24
25 touch-area := TouchArea { }
26
27 text-label := Text {
28 text: root.value;
29 vertical-alignment: center;
30 horizontal-alignment: center;
31 color: root.style.foreground;
32 font-size: root.style.font-size;
33 font-weight: root.style.font-weight;
34 }
35
36 states [
37 selected when root.selected: {
38 text-label.color: root.style.foreground-selected;
39 }
40 ]
41}
42
43export struct ClockStyle {
44 background: brush,
45 foreground: brush,
46 time-selector-style: TimeSelectorStyle
47}
48
49export component Clock {
50 in property <[int]> model;
51 in property <bool> two-columns;
52 in property <ClockStyle> style;
53 in property <int> total;
54 in-out property <int> current-item;
55 in property <int> current-value;
56
57 callback current-item-changed(index: int);
58
59 property <length> radius: max(root.width, root.height) / 2;
60 property <length> picker-ditameter: 48px;
61 property <length> center: root.radius - root.picker-ditameter / 2;
62 property <length> outer-padding: 2px;
63 property <length> inner-padding: 32px;
64 property <length> radius-outer: root.center - root.outer-padding;
65 property <length> radius-inner: root.center - root.inner-padding;
66 property <int> half-total: root.total / 2;
67 property <angle> rotation: 0.25turn;
68 property <length> current-x: get-index-x(root.current-value);
69 property <length> current-y: get-index-y(root.current-value);
70
71 min-width: 256px;
72 min-height: 256px;
73 vertical-stretch: 0;
74 horizontal-stretch: 0;
75
76 background-layer := Rectangle {
77 border-radius: max(self.width, self.height) / 2;
78 background: root.style.background;
79 }
80
81 if root.current-item >= 0 || root.current-item < root.model.length: Path {
82 stroke: root.style.foreground;
83 stroke-width: 2px;
84 viewbox-width: self.width / 1px;
85 viewbox-height: self.height / 1px;
86
87 MoveTo {
88 x: root.width / 2px;
89 y: root.height / 2px;
90 }
91
92 LineTo {
93 x: (root.current-x + root.picker-ditameter / 2) / 1px;
94 y: (root.current-y + root.picker-ditameter / 2) / 1px;
95 }
96 }
97
98 Rectangle {
99 width: 8px;
100 height: 8px;
101 background: root.style.foreground;
102 border-radius: 4px;
103 }
104
105 if root.current-item < root.model.length: Rectangle {
106 x: root.current-x;
107 y: root.current-y;
108 width: root.picker-ditameter;
109 height: root.picker-ditameter;
110 border-radius: root.picker-ditameter / 2;
111 background: root.style.foreground;
112
113 if root.current-item < 0: Rectangle {
114 width: 4px;
115 height: 4px;
116 border-radius: 2px;
117 background: root.style.time-selector-style.foreground;
118 }
119 }
120
121 for val[index] in root.model: TimeSelector {
122 x: get-index-x(val);
123 y: get-index-y(val);
124 width: root.picker-ditameter;
125 height: root.picker-ditameter;
126 value: val;
127 selected: index == root.current-item;
128 style: root.style.time-selector-style;
129 accessible-role: button;
130 accessible-label: @tr("{} Hours or minutes of {}", val, root.total);
131 accessible-action-default => {
132 self.clicked();
133 }
134
135 clicked => {
136 root.set-current-item(index);
137 }
138 }
139
140 pure function value-to-angle(value: int) -> angle {
141 if root.two-columns {
142 if value >= root.half-total {
143 return clamp((value - root.half-total) / root.half-total * 1turn, 0, 0.999999turn) - root.rotation;
144 }
145 return clamp(value / root.half-total * 1turn, 0, 0.99999turn) - root.rotation;
146 }
147 clamp(value / root.total * 1turn, 0, 0.99999turn) - root.rotation;
148 }
149
150 pure function get-index-x(value: int) -> length {
151 if root.two-columns && value >= root.half-total {
152 return root.center + (root.radius-inner / 1px * cos(root.value-to-angle(value))) * 1px;
153 }
154 root.center + (root.radius-outer / 1px * cos(root.value-to-angle(value))) * 1px
155 }
156
157 pure function get-index-y(value: int) -> length {
158 // this is only for 24 mode
159 if root.total == 24 && value == 0 {
160 return root.center + (root.radius-inner / 1px * sin(root.value-to-angle(value))) * 1px;
161 }
162 if root.total == 24 && value == 12 {
163 return root.center + (root.radius-outer / 1px * sin(root.value-to-angle(value))) * 1px;
164 }
165 if root.two-columns && value >= root.half-total {
166 return root.center + (root.radius-inner / 1px * sin(root.value-to-angle(value))) * 1px;
167 }
168 root.center + (root.radius-outer / 1px * sin(root.value-to-angle(value))) * 1px
169 }
170
171 function set-current-item(index: int) {
172 root.current-item-changed(index);
173 }
174}
175
176export struct TimePickerInputStyle {
177 background: brush,
178 background-selected: brush,
179 foreground: brush,
180 foreground-selected: brush,
181 border-radius: length,
182 font-size: length,
183 font-weight: float
184}
185
186export component TimePickerInput {
187 in property <TimePickerInputStyle> style;
188 in property <bool> read-only <=> text-input.read-only;
189 in property <bool> checked;
190 in-out property <string> text <=> text-input.text;
191
192 callback clicked;
193 callback edited(int);
194
195 min-width: max(96px, text-input.min-width);
196 min-height: max(80px, text-input.min-height);
197 vertical-stretch: 0;
198 horizontal-stretch: 0;
199
200 forward-focus: text-input;
201
202 background-layer := Rectangle {
203 border-radius: root.style.border-radius;
204 background: root.style.background;
205 }
206
207 text-input := TextInput {
208 vertical-alignment: center;
209 horizontal-alignment: center;
210 width: 100%;
211 height: 100%;
212 color: root.style.foreground;
213 font-size: root.style.font-size;
214 font-weight: root.style.font-weight;
215 input-type: number;
216 edited => {
217 root.edited(self.text.to-float());
218 }
219 }
220
221 if root.read-only: TouchArea {
222 clicked => {
223 root.clicked();
224 }
225 }
226
227 states [
228 checked when root.checked: {
229 background-layer.background: root.style.background-selected;
230 text-input.color: root.style.foreground-selected;
231 }
232 ]
233}
234
235export struct PeriodSelectorItemStyle {
236 font-size: length,
237 font-weight: float,
238 foreground: brush,
239 background-selected: brush,
240 foreground-selected: brush
241}
242
243export component PeriodSelectorItem {
244 in property <PeriodSelectorItemStyle> style;
245 in property <string> text <=> label.text;
246 in property <bool> checked;
247
248 callback clicked <=> touch-area.clicked;
249
250 touch-area := TouchArea { }
251
252 background-layer := Rectangle { }
253
254 label := Text {
255 font-size: root.style.font-size;
256 font-weight: root.style.font-weight;
257 color: root.style.foreground;
258 horizontal-alignment: center;
259 }
260
261 states [
262 checked when root.checked: {
263 background-layer.background: root.style.background-selected;
264 label.color: root.style.foreground-selected;
265 }
266 ]
267}
268
269export struct PeriodSelectorStyle {
270 item-style: PeriodSelectorItemStyle,
271 border-brush: brush,
272 border-radius: length,
273 border-width: length}
274
275export component PeriodSelector {
276 in property <PeriodSelectorStyle> style;
277 in property <bool> am-selected;
278
279 callback update-period(bool);
280
281 min-width: max(38px, layout.min-width);
282 accessible-label: "AM or PM";
283 accessible-role: checkbox;
284 accessible-checked: root.am-selected;
285
286 Rectangle {
287 border-radius: border.border-radius;
288 clip: true;
289
290 layout := VerticalLayout {
291 PeriodSelectorItem {
292 text: "AM";
293 checked: root.am-selected;
294 style: root.style.item-style;
295
296 clicked => {
297 if root.am-selected {
298 return;
299 }
300 root.update-period(true);
301 }
302 }
303
304 Rectangle {
305 height: 1px;
306 background: border.border-color;
307 vertical-stretch: 0;
308 }
309
310 PeriodSelectorItem {
311 text: "PM";
312 checked: !root.am-selected;
313 style: root.style.item-style;
314
315 clicked => {
316 if !root.am-selected {
317 return;
318 }
319 root.update-period(false);
320 }
321 }
322 }
323 }
324
325 border := Rectangle {
326 border-radius: root.style.border-radius;
327 border-width: root.style.border-width;
328 border-color: root.style.border-brush;
329 }
330}
331
332export struct Time {
333 hour: int,
334 minute: int,
335 second: int
336}
337
338export struct TimePickerStyle {
339 foreground: brush,
340 horizontal-spacing: length,
341 vertical-spacing: length,
342 clock-style: ClockStyle,
343 input-style: TimePickerInputStyle,
344 period-selector-style: PeriodSelectorStyle,
345 title-style: ColoredTextStyle,
346}
347
348export component TimePickerBase {
349 in property <bool> use-24-hour-format: SlintInternal.use-24-hour-format;
350 in property <bool> selection-mode: true;
351 in property <TimePickerStyle> style;
352 in property <Time> time: { hour: 12 };
353 in property <string> title;
354
355 property <bool> minutes-selected;
356 property <bool> am-selected: true;
357 property <[int]> twelf-hour-model: [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
358 property <[int]> use-24-hour-format-model: [
359 12,
360 1,
361 2,
362 3,
363 4,
364 5,
365 6,
366 7,
367 8,
368 9,
369 10,
370 11,
371 0,
372 13,
373 14,
374 15,
375 16,
376 17,
377 18,
378 19,
379 20,
380 21,
381 22,
382 23
383 ];
384 property <[int]> minute-model: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
385 property <[int]> current-model: root.get-current-model();
386 property <int> current-item: root.minutes-selected ? root.index-of-minute(root.current-time.minute) : root.index-of-hour(root.current-time.hour);
387 property <Time> current-time: root.time;
388 property <int> time-picker-hour: hour-time-picker.text.to-float();
389 property <int> time-picker-minute: minute-time-picker.text.to-float();
390
391 min-width: content-layer.min-width;
392 min-height: content-layer.min-height;
393
394 content-layer := VerticalLayout {
395 spacing: root.style.vertical-spacing;
396
397 HorizontalLayout {
398 Text {
399 text: root.title;
400 horizontal-alignment: left;
401 overflow: elide;
402 font-size: root.style.title-style.font-size;
403 font-weight: root.style.title-style.font-weight;
404 color: root.style.title-style.foreground;
405 }
406 }
407
408 HorizontalLayout {
409 spacing: root.style.horizontal-spacing;
410 alignment: center;
411
412 hour-time-picker := TimePickerInput {
413 read-only: root.selection-mode;
414 checked: self.read-only && !root.minutes-selected;
415 text: root.current-time.hour;
416 style: root.style.input-style;
417
418 accessible-role: AccessibleRole.text-input;
419 accessible-value: self.text;
420 accessible-label: "hour";
421
422 clicked => {
423 root.minutes-selected = false;
424 }
425 }
426
427 separator := Text {
428 text: ":";
429 color: root.style.foreground;
430 font-size: root.style.input-style.font-size;
431 font-weight: root.style.input-style.font-weight;
432 vertical-alignment: center;
433 }
434
435 minute-time-picker := TimePickerInput {
436 read-only: root.selection-mode;
437 checked: self.read-only && root.minutes-selected;
438 text: root.current-time.minute;
439 style: root.style.input-style;
440
441 accessible-role: AccessibleRole.text-input;
442 accessible-value: self.text;
443 accessible-label: "minute";
444
445 clicked => {
446 root.minutes-selected = true;
447 }
448 }
449
450 if !root.use-24-hour-format: PeriodSelector {
451 am-selected: root.am-selected;
452 style: root.style.period-selector-style;
453
454 update-period(am) => {
455 root.am-selected = am;
456 }
457 }
458 }
459
460 if root.selection-mode: HorizontalLayout {
461 alignment: center;
462
463 Clock {
464 width: 256px;
465 height: 256px;
466 model: root.current-model;
467 style: root.style.clock-style;
468 two-columns: !root.minutes-selected && root.use-24-hour-format;
469 current-item: root.current-item;
470 total: root.minutes-selected ? 60 : root.use-24-hour-format ? 24 : 12;
471 current-value: root.minutes-selected ? root.current-time.minute : root.current-time.hour;
472
473 current-item-changed(index) => {
474 root.update-time-by-item(index);
475
476 if !root.minutes-selected {
477 root.minutes-selected = true;
478 }
479 }
480 }
481 }
482 }
483
484 pure public function ok-enabled() -> bool {
485 if root.selection-mode {
486 return root.current-time.hour <= 23 && root.current-time.minute <= 59;
487 }
488 if root.use-24-hour-format {
489 return root.time-picker-hour <= 23 && root.time-picker-minute <= 59;
490 }
491 root.time-picker-hour <= 12 && root.time-picker-minute <= 59
492 }
493
494 public function get-current-time() -> Time {
495 if root.selection-mode {
496 if !root.use-24-hour-format && !root.am-selected {
497 if root.current-time.hour == 12 {
498 return { hour: 0, minute: root.current-time.minute };
499 }
500 return { hour: root.current-time.hour + 12, minute: root.current-time.minute };
501 }
502 return root.current-time;
503 }
504 return { hour: root.time-picker-hour, minute: root.time-picker-minute };
505 }
506
507 changed selection-mode => {
508 if !root.selection-mode {
509 return;
510 }
511
512 root.update-time(min(root.time-picker-hour, root.use-24-hour-format ? 23 : 12), min(root.time-picker-minute, 59));
513 }
514
515 changed time => {
516 root.current-time = root.time;
517 }
518
519 function update-time-by-item(index: int) {
520 root.update-time-by-value(root.current-model[index]);
521 }
522
523 function update-time-by-value(value: int) {
524 if root.minutes-selected {
525 root.update-time(root.current-time.hour, value);
526 return;
527 }
528 root.update-time(value, root.current-time.minute)
529 }
530
531 function update-time(hour: int, minute: int) {
532 root.current-time = { hour: hour, minute: minute };
533 hour-time-picker.text = root.current-time.hour;
534 minute-time-picker.text = minute;
535 }
536
537 pure function index-of-minute(minute: int) -> int {
538 if mod(minute, 5) != 0 {
539 return -1;
540 }
541 minute / 5
542 }
543
544 pure function index-of-hour(hour: int) -> int {
545 if hour == 12 {
546 return 0;
547 }
548 if hour == 0 {
549 return 12;
550 }
551 if !root.use-24-hour-format && hour > 12 {
552 return hour - 12;
553 }
554 return hour;
555 }
556
557 pure function get-current-model() -> [int] {
558 if root.minutes-selected {
559 return root.minute-model;
560 }
561 if root.use-24-hour-format {
562 return root.use-24-hour-format-model;
563 }
564 root.twelf-hour-model;
565 }
566}
567