CKGreeter/ui/window.slint
2026-05-08 23:30:01 +02:00

509 lines
20 KiB
Plaintext

import { ScrollView, Button, TabWidget } from "std-widgets.slint";
import { Data, LoginOptionTileData } from "data.slint";
import { LoginOptionTile } from "envtile.slint";
import { LineEdit } from "widgets/fluent/lineedit.slint";
import { IconButton } from "iconbutton.slint";
export { Data as GreeterData } from "data.slint";
export component GreeterDisplay inherits Window {
changed width => {
self.default-font-size = Data.get-default-font-size();
login_manager_underlay.width = max(self.width * 0.33, 640px);
login_manager_underlay.height = self.height;
}
changed height => {
self.default-font-size = Data.get-default-font-size();
login_manager_underlay.width = max(self.width * 0.33, 640px);
login_manager_underlay.height = self.height;
}
default-font-size: Data.get-default-font-size();
in-out property <int> selected_index <=> Data.selected_index;
in-out property <int> error_count <=> Data.error_count;
in-out property <string> error_content <=> error_popup.content;
in property <[LoginOptionTileData]> tiles;
in property <string> default_username;
out property <bool> background-toggle;
public function set_error(message: string) {
error_popup.content = message;
error_count += 1;
error_popup.opacity = 1.0;
error_popup_timer.start();
error_popup_timer.restart();
}
public function switch_background() {
background-toggle = !background-toggle;
background_timer.start();
clear_inactive_wallpaper_timer.start();
background_timer.restart();
clear_inactive_wallpaper_timer.restart();
}
full-screen: true;
preferred-width: 1920px;
preferred-height: 1080px;
min-width: 640px;
min-height: 480px;
TouchArea {
clicked => {
confirm_prompt.visible = false;
}
backdrop := Rectangle { // Almost black backdrop, in case background can't load
background: rgba(20, 20, 40);
z: -3;
}
background := Image { // Image background
z: -2;
animate opacity {
duration: 1000ms;
easing: ease-in-out;
}
source: Data.wallpaper-image;
image-fit: ImageFit.cover;
opacity: background-toggle && self.source.width > 0 ? 1.0 : 0.0;
background_timer := Timer {
interval: Data.wallpaper-cooldown * 1s;
running: true;
triggered() => {
Data.get-new-wallpaper();
self.running = false;
}
}
clear_inactive_wallpaper_timer := Timer {
interval: 1200ms;
running: false;
triggered() => {
Data.clear-inactive-wallpaper();
self.running = false;
}
}
}
background_1 := Image { // Background (secondary), needed for smooth opacity transitions
z: -2;
animate opacity {
duration: 1000ms;
easing: ease-in-out;
}
source: Data.wallpaper-image-1;
image-fit: ImageFit.cover;
opacity: !background-toggle && self.source.width > 0 ? 1.0 : 0.0;
}
background_overlay := Rectangle { // Background overlay
background: rgba(0, 0, 0, 0.5);
opacity: background.opacity > 0 || background_1.opacity > 0 ? 1.0 : 0.0;
z: -1;
}
image_info := Rectangle {
opacity: 0.6;
x: 1rem;
y: root.height - 7.5rem;
height: 6.5rem;
width: 33%;
private property <bool> show_extended;
VerticalLayout {
alignment: end;
spacing: 1rem;
img_link_info := HorizontalLayout {
visible: show_extended;
spacing: 0.5rem;
Image {
source: @image-url("icons/link.svg");
height: 1.25rem;
width: 1.25rem;
}
Text {
text: Data.wallpaper-url;
font-size: 1.25rem;
font-family: "monospace";
color: rgba(240, 240, 240, 1);
}
}
img_text_info := HorizontalLayout {
visible: show_extended;
spacing: 0.5rem;
Image {
source: @image-url("icons/text.svg");
height: 1.25rem;
width: 1.25rem;
}
Text {
text: Data.wallpaper-alt;
font-size: 1.25rem;
font-family: "monospace";
color: rgba(240, 240, 240, 1);
}
}
img_author_info := HorizontalLayout {
visible: true;
spacing: 0.5rem;
Image {
source: @image-url("icons/person.svg");
height: 1.25rem;
width: 1.25rem;
}
Text {
text: Data.wallpaper-author;
font-size: 1.25rem;
font-family: "monospace";
color: Data.wallpaper-author == "Error" ? rgba(255, 0, 0, 1) : rgba(240, 240, 240, 1);
}
}
}
TouchArea {
clicked => {
show_extended = !show_extended
}
}
}
login_manager_underlay := Rectangle {
background: rgba(0, 0, 0, 0.5);
drop-shadow-blur: 4rem;
height: 0px;
width: 0px;
animate width {
duration: 500ms;
easing: ease-in-sine;
}
z: -1;
VerticalLayout {
clock := VerticalLayout {
alignment: center;
height: 15%;
HorizontalLayout {
alignment: center;
Rectangle {
y: 10rem;
width: 32rem;
height: 12rem;
VerticalLayout {
padding-top: 1rem;
Rectangle {
Text {
x: time_text.x - 0.1rem; // Offset
y: time_text.y + 0.1rem; // Offset
text: Data.current-time;
font-size: 6.4rem;
horizontal-alignment: center;
color: rgba(0, 0, 0, 1);
}
time_text := Text {
text: Data.current-time;
font-size: 6.4rem;
horizontal-alignment: center;
}
}
Rectangle {
Text {
x: date_text.x - 0.1rem; // Offset
y: date_text.y + 0.1rem; // Offset
text: date_text.text;
font-size: date_text.font-size;
horizontal-alignment: date_text.horizontal-alignment;
color: rgba(0, 0, 0, 1);
}
date_text := Text {
text: Data.current-date;
font-size: 1.6rem;
horizontal-alignment: center;
}
}
}
}
}
}
login_model := VerticalLayout {
alignment: center;
height: 70%;
spacing: 1rem;
init_timer := Timer {
interval: 0s;
running: true;
triggered => {
if (!Data.has-default-username) {
username_box.focus();
} else {
password_box.focus();
}
self.running = false;
}
}
environment_selector := FocusScope {
height: 25%;
key-pressed(event) => {
if (event.text == Key.LeftArrow || event.text == "a" || event.text == "A") {
if (Data.selected_index == 0) {
Data.selected_index = tiles.length - 1;
} else {
Data.selected_index -= 1;
}
accept
} else if (event.text == Key.RightArrow || event.text == "d" || event.text == "D") {
if (Data.selected_index + 1 == tiles.length) {
Data.selected_index = 0;
} else {
Data.selected_index += 1;
}
accept
}
reject
}
scroll_view := ScrollView {
width: min(((self.height + 1rem) * tiles.length) - 1rem, parent.width);
animate viewport-x { duration: 250ms; }
viewport-width: ((self.height + 1rem) * tiles.length) - 1rem;
viewport-x: (self.viewport-width - self.height) / 2 - (Data.selected_index * (self.height));
scroll_layout := HorizontalLayout {
height: parent.height;
for tile[i] in tiles: LoginOptionTile {
width: parent.height;
icon: tile.image;
label: tile.name;
index: i;
has_focus: environment_selector.has-focus;
on_click => {
Data.selected_index = self.index;
}
}
}
}
}
// Username field
HorizontalLayout {
height: 2.5rem;
alignment: center;
username_box := LineEdit {
font-size: 2rem;
font-family: "monospace";
width: 45%;
placeholder-text: "Username";
text: default_username;
border-color: rgba(185, 15, 220, 0.85);
on-focus-change => {
Data.check-lock-states()
}
key-released => {
Data.check-lock-states();
return EventResult.accept;
}
accepted(text) => {
Data.login(username_box.text, password_box.text)
}
}
}
// Password field
HorizontalLayout {
height: 2.5rem;
alignment: center;
password_box := LineEdit {
font-size: 2rem;
font-family: "monospace";
width: username_box.width;
placeholder-text: "Password";
input-type: password;
border-color: rgba(185, 15, 220, 0.85);
on-focus-change => {
Data.check-lock-states()
}
key-released => {
Data.check-lock-states();
return EventResult.accept;
}
accepted(text) => {
Data.login(username_box.text, password_box.text)
}
}
}
// Submit button
HorizontalLayout {
height: 2.5rem;
alignment: center;
IconButton {
width: username_box.width;
icon: @image-url("icons/right_arrow.svg");
icon-width: 4rem;
icon-height: self.icon-width/3;
icon-align: LayoutAlignment.end;
text: "Login";
font-size: 1.5rem;
clicked => {
Data.login(username_box.text, password_box.text);
}
}
}
}
power_model := VerticalLayout {
height: 15%;
confirm_prompt := Text {
horizontal-alignment: center;
font-size: 1.5rem;
text: "Press again to confirm ";
visible: false;
vertical-alignment: bottom;
confirm_prompt_timer := Timer {
interval: 5s;
running: false;
triggered() => {
confirm_prompt.visible = false;
self.running = false;
}
}
}
HorizontalLayout {
alignment: center;
Rectangle {
background: rgba(0, 0, 0, 0.6);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
width: 16.3rem;
height: 5.8rem;
// Power action buttons
HorizontalLayout {
private property <string> lastaction;
padding-top: 0.4rem;
alignment: center;
spacing: 0.5rem;
function power_action(action: string, command: string) {
if (lastaction != action || confirm_prompt.visible == false) {
lastaction = action;
confirm_prompt.text = "Press again to confirm " + action;
confirm_prompt.visible = true;
confirm_prompt_timer.start();
confirm_prompt_timer.restart();
} else {
if (lastaction == action) {
Data.loginctl(command)
} else {
confirm_prompt.visible = false;
}
}
}
IconButton {
width: 4.8rem;
height: 4.8rem;
icon: @image-url("icons/power.svg");
icon-width: self.width * 0.5;
icon-height: self.width * 0.5;
clicked => {
power_action("poweroff", "poweroff")
}
}
IconButton {
width: 4.8rem;
height: 4.8rem;
icon: @image-url("icons/restart.svg");
icon-width: self.width * 0.5;
icon-height: self.width * 0.5;
clicked => {
power_action("restart", "reboot")
}
}
IconButton {
width: 4.8rem;
height: 4.8rem;
icon: @image-url("icons/sleep.svg");
icon-width: self.width * 0.5;
icon-height: self.width * 0.5;
clicked => {
power_action("sleep", "suspend")
}
}
}
}
}
}
}
}
error_popup := Rectangle {
background: rgba(220, 50, 50, 0.85);
width: 25%;
height: 12.5%;
y: (parent.height * 0.025);
opacity: 0.0;
animate opacity { duration: 250ms; }
in-out property <string> content;
Text {
color: rgba(240, 240, 240, 1);
width: 85%;
height: 85%;
vertical-alignment: center;
horizontal-alignment: center;
font-family: "monospace";
font-size: max(1rem, scroll_view.height * 0.125);
text: "[" + Data.error_count + "] " + content;
}
}
error_popup_timer := Timer {
interval: 5s;
running: false;
triggered() => {
error_popup.opacity = 0.0;
self.running = false;
}
}
}
}