breadgreet: bring the login UI up to the lock screen's polish
Some checks failed
CI / check (pull_request) Failing after 3s
Some checks failed
CI / check (pull_request) Failing after 3s
breadgreet's stylesheet was a crude subset of what `design/sketch.html` specifies for `.greetoverlay`. Ported it faithfully so the greeter and the lock screen read as one visual identity: - Card: proper elevation (`box-shadow: 0 6px 28px`), a hairline `alpha(@overlay, 0.09)` border, 10px radius, 300px min-width. - Entry: recessed `shade(@surface, 1.5)` fill, and an accent focus ring (`border-color: @accent` + `box-shadow: 0 0 0 2px alpha(@accent, .28)`) matching the sketch's `.gentry:focus`. - Clock/date over the wallpaper get a soft text-shadow for legibility on any background; sizes match the sketch (46 / 14). - Session picker styled as a surface pill (button + popover), not a bare GtkDropDown. - A CSS-animated ring shown while a greetd request is in flight (`sync_busy`, driven from the single `update` exit point). - Shake on a failed attempt (`flash_error`), matching the lock screen's wrong-password motion — fired from both `AppInput::Error` and a PAM `AuthPrompt::Error`. Uses the shared `@define-color` palette (`@surface`, `@overlay`, `@accent`, `@red`) so it recolours with the theme, same as breadbar. `cargo test --workspace --all-targets` (157) / `clippy --all-targets -D warnings` / `fmt --check` all clean. Visual pass still wanted — the CSS is a direct translation of the mockup, not screenshot-verified.
This commit is contained in:
parent
758a5d9f9c
commit
7ac3fdfc85
2 changed files with 145 additions and 19 deletions
|
|
@ -46,6 +46,8 @@ struct App {
|
||||||
date_lbl: gtk4::Label,
|
date_lbl: gtk4::Label,
|
||||||
status_lbl: gtk4::Label,
|
status_lbl: gtk4::Label,
|
||||||
entry: gtk4::Entry,
|
entry: gtk4::Entry,
|
||||||
|
card: gtk4::Box,
|
||||||
|
spinner: gtk4::Box,
|
||||||
stage: Stage,
|
stage: Stage,
|
||||||
username: String,
|
username: String,
|
||||||
sessions: Vec<sessions::Session>,
|
sessions: Vec<sessions::Session>,
|
||||||
|
|
@ -168,10 +170,18 @@ impl SimpleComponent for App {
|
||||||
dropdown.upcast()
|
dropdown.upcast()
|
||||||
};
|
};
|
||||||
|
|
||||||
let card = gtk4::Box::new(gtk4::Orientation::Vertical, 8);
|
// A CSS-animated ring (not GtkSpinner) so it matches the design
|
||||||
|
// sketch exactly: 2px track, accent top, shown only while a greetd
|
||||||
|
// request is in flight (see `set_busy`).
|
||||||
|
let spinner = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||||
|
spinner.add_css_class("login-spinner");
|
||||||
|
spinner.set_halign(gtk4::Align::Center);
|
||||||
|
|
||||||
|
let card = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
|
||||||
card.add_css_class("login-card");
|
card.add_css_class("login-card");
|
||||||
card.append(&entry);
|
card.append(&entry);
|
||||||
card.append(&status_lbl);
|
card.append(&status_lbl);
|
||||||
|
card.append(&spinner);
|
||||||
card.append(&session_widget);
|
card.append(&session_widget);
|
||||||
|
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
|
@ -243,6 +253,8 @@ impl SimpleComponent for App {
|
||||||
date_lbl,
|
date_lbl,
|
||||||
status_lbl,
|
status_lbl,
|
||||||
entry,
|
entry,
|
||||||
|
card,
|
||||||
|
spinner,
|
||||||
stage: Stage::Username,
|
stage: Stage::Username,
|
||||||
username: String::new(),
|
username: String::new(),
|
||||||
sessions,
|
sessions,
|
||||||
|
|
@ -289,10 +301,38 @@ impl SimpleComponent for App {
|
||||||
}
|
}
|
||||||
AppInput::Cancel => self.cancel_auth(),
|
AppInput::Cancel => self.cancel_auth(),
|
||||||
}
|
}
|
||||||
|
self.sync_busy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
/// Spinner runs exactly while a greetd request is in flight. Driven from
|
||||||
|
/// one place (the end of `update`) so every path — submit, prompt,
|
||||||
|
/// cancel, error — stays in sync with `self.stage`.
|
||||||
|
fn sync_busy(&self) {
|
||||||
|
let busy = matches!(self.stage, Stage::Working | Stage::Starting);
|
||||||
|
if busy {
|
||||||
|
self.spinner.add_css_class("spinning");
|
||||||
|
} else {
|
||||||
|
self.spinner.remove_css_class("spinning");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-shot shake on the card, matching the lock screen's wrong-password
|
||||||
|
/// motion. Re-armed each call by dropping the class first (a running
|
||||||
|
/// animation won't restart just from re-adding it).
|
||||||
|
fn flash_error(&self) {
|
||||||
|
self.card.remove_css_class("shake");
|
||||||
|
let card = self.card.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(10), move || {
|
||||||
|
card.add_css_class("shake");
|
||||||
|
let card2 = card.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(std::time::Duration::from_millis(420), move || {
|
||||||
|
card2.remove_css_class("shake");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_submit(&mut self) {
|
fn handle_submit(&mut self) {
|
||||||
if matches!(self.stage, Stage::Working | Stage::Starting) {
|
if matches!(self.stage, Stage::Working | Stage::Starting) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -338,6 +378,7 @@ impl App {
|
||||||
AuthPrompt::Error(message) => {
|
AuthPrompt::Error(message) => {
|
||||||
self.status_lbl.add_css_class("error");
|
self.status_lbl.add_css_class("error");
|
||||||
self.status_lbl.set_label(&message);
|
self.status_lbl.set_label(&message);
|
||||||
|
self.flash_error();
|
||||||
self.pam_status_held = true;
|
self.pam_status_held = true;
|
||||||
self.dispatch(greetd::Command::Respond(None));
|
self.dispatch(greetd::Command::Respond(None));
|
||||||
}
|
}
|
||||||
|
|
@ -403,6 +444,7 @@ impl App {
|
||||||
self.status_lbl.remove_css_class("error");
|
self.status_lbl.remove_css_class("error");
|
||||||
} else {
|
} else {
|
||||||
self.status_lbl.add_css_class("error");
|
self.status_lbl.add_css_class("error");
|
||||||
|
self.flash_error();
|
||||||
}
|
}
|
||||||
self.reset_to_username();
|
self.reset_to_username();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
use bread_theme::{gtk as bgtk, ink_on, load_palette};
|
//! breadgreet's app stylesheet.
|
||||||
|
//!
|
||||||
|
//! Layered on top of `bread_theme`'s shared `@define-color` palette
|
||||||
|
//! (`@surface`, `@overlay`, `@accent`, `@red`, …) via `apply_shared()` +
|
||||||
|
//! `apply_app_css()`. The visual target is `design/sketch.html`'s
|
||||||
|
//! `.greetoverlay` section — the same design language as the lock screen:
|
||||||
|
//! a floating surface card over a cover-fit / Ken-Burns wallpaper, an
|
||||||
|
//! accent focus ring on the entry, a spinner while a request is in flight,
|
||||||
|
//! and a shake on a failed attempt.
|
||||||
|
|
||||||
|
use bread_theme::gtk as bgtk;
|
||||||
use gtk4::CssProvider;
|
use gtk4::CssProvider;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
|
@ -15,25 +25,99 @@ fn css_font_family(family: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_css(font_family: &str) -> String {
|
fn load_css(font_family: &str) -> String {
|
||||||
let p = load_palette();
|
|
||||||
let font = css_font_family(font_family);
|
let font = css_font_family(font_family);
|
||||||
format!(
|
format!(
|
||||||
"window.breadgreet {{ background-color: {bg}; color: {on_bg}; {font} }}\
|
// ---- window + wallpaper ----------------------------------------
|
||||||
.login-card {{ background: {surface}; color: {on_surface}; border-radius: 8px;\
|
"window.breadgreet {{ background-color: @bg; color: @on-bg; {font} }}\
|
||||||
padding: 20px; min-width: 320px; }}\
|
.login-veil {{\
|
||||||
.login-clock {{ font-size: 48px; font-weight: bold; }}\
|
background-image: linear-gradient(to bottom,\
|
||||||
.login-date {{ font-size: 18px; font-weight: 500; opacity: 0.8; }}\
|
alpha(black, 0.42) 0%, alpha(black, 0.20) 45%, alpha(black, 0.34) 100%);\
|
||||||
.login-entry {{ font-size: 14px; }}\
|
}}\
|
||||||
.login-status {{ font-size: 12px; opacity: 0.75; margin-top: 8px; }}\
|
\
|
||||||
.login-status.error {{ color: {red}; opacity: 1; }}\
|
/* ---- clock cluster (over the wallpaper) --------------------- */\
|
||||||
.login-session {{ font-size: 12px; opacity: 0.85; margin-top: 12px; }}\
|
.login-clock {{\
|
||||||
dropdown.login-session {{ min-height: 32px; }}\
|
font-size: 46px; font-weight: 700; color: white;\
|
||||||
.login-veil {{ background-image: linear-gradient(to bottom, rgba(0,0,0,0.34) 0%, rgba(0,0,0,0.16) 100%); }}",
|
text-shadow: 0 2px 12px alpha(black, 0.55);\
|
||||||
bg = p.background,
|
}}\
|
||||||
surface = p.color0,
|
.login-date {{\
|
||||||
red = p.color1,
|
font-size: 14px; font-weight: 500; color: alpha(white, 0.82);\
|
||||||
on_bg = ink_on(&p.background),
|
text-shadow: 0 1px 8px alpha(black, 0.5);\
|
||||||
on_surface = ink_on(&p.color0),
|
}}\
|
||||||
|
\
|
||||||
|
/* ---- the card --------------------------------------------- */\
|
||||||
|
.login-card {{\
|
||||||
|
background: @surface; color: @on-surface;\
|
||||||
|
border: 1px solid alpha(@overlay, 0.09);\
|
||||||
|
border-radius: 10px; padding: 22px;\
|
||||||
|
min-width: 300px;\
|
||||||
|
box-shadow: 0 6px 28px alpha(black, 0.5);\
|
||||||
|
}}\
|
||||||
|
.login-card.shake {{ animation: breadgreet-shake 380ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }}\
|
||||||
|
\
|
||||||
|
/* ---- entry ----------------------------------------------- */\
|
||||||
|
.login-entry {{\
|
||||||
|
background: shade(@surface, 1.5); color: @on-surface;\
|
||||||
|
border: 1px solid alpha(@overlay, 0.14);\
|
||||||
|
border-radius: 7px; padding: 11px 14px; font-size: 14px;\
|
||||||
|
caret-color: @accent;\
|
||||||
|
transition: border-color 180ms ease, box-shadow 180ms ease;\
|
||||||
|
}}\
|
||||||
|
.login-entry:disabled {{ opacity: 0.55; }}\
|
||||||
|
.login-entry > text {{ background: transparent; }}\
|
||||||
|
.login-entry:focus-within {{\
|
||||||
|
border-color: @accent;\
|
||||||
|
box-shadow: 0 0 0 2px alpha(@accent, 0.28);\
|
||||||
|
outline: none;\
|
||||||
|
}}\
|
||||||
|
\
|
||||||
|
/* ---- status line --------------------------------------- */\
|
||||||
|
.login-status {{\
|
||||||
|
font-size: 12px; color: alpha(@on-surface, 0.72);\
|
||||||
|
margin-top: 10px; min-height: 1em;\
|
||||||
|
transition: color 160ms ease;\
|
||||||
|
}}\
|
||||||
|
.login-status.error {{ color: @red; opacity: 1; font-weight: 700; }}\
|
||||||
|
\
|
||||||
|
/* ---- spinner (shown while a request is in flight) ------ */\
|
||||||
|
.login-spinner {{\
|
||||||
|
min-width: 16px; min-height: 16px;\
|
||||||
|
margin-top: 12px;\
|
||||||
|
border: 2px solid alpha(@overlay, 0.15);\
|
||||||
|
border-top: 2px solid @accent;\
|
||||||
|
border-radius: 999px;\
|
||||||
|
opacity: 0;\
|
||||||
|
}}\
|
||||||
|
.login-spinner.spinning {{ opacity: 1; animation: breadgreet-spin 800ms linear infinite; }}\
|
||||||
|
\
|
||||||
|
/* ---- session picker (styled as a surface pill) -------- */\
|
||||||
|
.login-session {{\
|
||||||
|
margin-top: 14px; font-size: 12px;\
|
||||||
|
}}\
|
||||||
|
.login-session > button {{\
|
||||||
|
background: shade(@surface, 1.5); color: alpha(@on-surface, 0.9);\
|
||||||
|
border: 1px solid alpha(@overlay, 0.14);\
|
||||||
|
border-radius: 7px; padding: 9px 12px; min-height: 20px;\
|
||||||
|
}}\
|
||||||
|
.login-session > button:hover {{ border-color: alpha(@accent, 0.5); }}\
|
||||||
|
.login-session > button:focus-within {{\
|
||||||
|
border-color: @accent; box-shadow: 0 0 0 2px alpha(@accent, 0.28); outline: none;\
|
||||||
|
}}\
|
||||||
|
.login-session arrow {{ color: alpha(@on-surface, 0.6); min-height: 16px; min-width: 16px; }}\
|
||||||
|
.login-session popover > contents {{\
|
||||||
|
background: @surface; border: 1px solid alpha(@overlay, 0.12);\
|
||||||
|
border-radius: 8px; padding: 4px;\
|
||||||
|
}}\
|
||||||
|
.login-session popover row {{ border-radius: 6px; padding: 6px 10px; }}\
|
||||||
|
.login-session popover row:selected {{ background: alpha(@accent, 0.22); color: @on-surface; }}\
|
||||||
|
\
|
||||||
|
/* ---- keyframes --------------------------------------- */\
|
||||||
|
@keyframes breadgreet-spin {{ to {{ transform: rotate(360deg); }} }}\
|
||||||
|
@keyframes breadgreet-shake {{\
|
||||||
|
10%, 90% {{ transform: translateX(-2px); }}\
|
||||||
|
20%, 80% {{ transform: translateX(4px); }}\
|
||||||
|
30%, 50%, 70% {{ transform: translateX(-7px); }}\
|
||||||
|
40%, 60% {{ transform: translateX(7px); }}\
|
||||||
|
}}",
|
||||||
font = font,
|
font = font,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue