Lock screen drawn cheaply: the backdrop (gradient, veil and vignette) is one shader, lock.wgsl, instead of a vello gradient every frame; every icon in one canvas; lock.look.vignette

This commit is contained in:
server2
2026-10-21 22:05:01 +02:00
parent 184da49c2f
commit 303764732b
5 changed files with 281 additions and 134 deletions
+3 -1
View File
@@ -130,6 +130,8 @@ Left out on purpose: phone stats (iOS shares nothing Linux can read without extr
The picture is `lock.look.wallpaper`, or else the wallpaper the colours came from: `matugen/amane.colors` writes its path into the theme file (`wallpaper=`), so it's there after the next wallpaper change. Without either, the colours' gradient. Amane reads PNG and JPEG only.
It redraws for the music's progress, the clock and typing, so it's cheap to draw: the backdrop (the gradient, and the veil over the picture, darker toward the corners, `lock.look.vignette`) is one shader, `src/lock.wgsl`, and every icon goes into one canvas. Measured in a preview on one 1080p monitor: 0.8 ms per frame to submit and present (2.4 ms before), the GPU's render engine half as busy.
Killing the shell does not unlock: Hyprland keeps the session locked and shows its own error screen. Keep a second TTY logged in when trying changes to the lock screen.
## Settings
@@ -195,7 +197,7 @@ src/glyph.rs every drawn icon, as a Canvas or as shapes for a shared one
src/theme.rs the shell's colours: the matugen theme file, the cross-fade; watches config.toml
src/lock.rs the lock screen (amane's App::lock), `lock` IPC
src/surface.rs what the island, pills, menus and popups are made of: the solid, frosted and liquid designs as values for surface.wgsl, glass for glass.wgsl
src/*.wgsl shaders: surface (every surface), ring (the candles), curve (the equalizer's line)
src/*.wgsl shaders: surface (every surface), glass (the glass design), ring (the candles), curve (the equalizer's line), lock (the lock screen's backdrop)
src/config.rs every setting, declared once (typed accessors + the window's list), config.toml read and written
src/hypr.rs Hyprland's options for the config window: hyprctl descriptions/getoption, hl.config, amane.lua
src/settings.rs the config window, `config` IPC
+1
View File
@@ -635,6 +635,7 @@ settings! {
blur: u64 = 6, 0..=40; "How blurred the picture is, in pixels of its small copy; 0 shows it sharp"
copy_width: u64 = 320, 64..=1920; "Width of the small copy of the picture that's blurred and stretched over the screen; smaller is softer"
dim: f32 = 0.3, 0..=1; "How much the picture is darkened, so the words over it stay readable"
vignette: f32 = 0.35, 0..=1; "How much darker the screen's corners are than its middle"
top: f32 = 90.0, 0..=600; "Space above the date"
bottom: f32 = 70.0, 0..=600; "Space below the password field"
gap: f32 = 16.0, 0..=80; "Space between the lock screen's parts"
+10 -1
View File
@@ -828,6 +828,11 @@ pub fn battery_width(charge: &Charge) -> f32 {
// a battery drawn from rectangles, filled to its charge, with a bolt while it charges
pub fn battery(charge: &Charge) -> Row {
battery_with(charge, true)
}
// `bolt` false leaves the bolt's place (11 px, first) empty, for a caller drawing its icons into one canvas
pub fn battery_with(charge: &Charge, bolt: bool) -> Row {
let theme = theme::now();
let low = charge.percent <= 15 && charge.status == "Discharging";
@@ -849,7 +854,11 @@ pub fn battery(charge: &Charge) -> Row {
let mut parts: Vec<Box<dyn Widget>> = Vec::new();
if charge.status == "Charging" {
parts.push(Box::new(icon(Glyph::Bolt, 11.0, theme.accent)));
if bolt {
parts.push(Box::new(icon(Glyph::Bolt, 11.0, theme.accent)));
} else {
parts.push(Box::new(Rectangle::new().width(11.0).height(11.0)));
}
}
parts.push(Box::new(Row::new(children![body, nub]).gap(1.0).align(Center)));
+231 -132
View File
@@ -1,12 +1,14 @@
use std::fs;
use std::path::PathBuf;
use std::sync::LazyLock;
use amane::{Center, Color, Column, Full, Gradient, Image, LayerWindow, Lock, Media, MediaPlayer, Monitor, Pointer, Rectangle, Row, Service, SpaceBetween, Stack, Start, Text, TextInput, Weight, Widget, children, window_size};
use amane::{Canvas, Center, Color, Column, Full, Image, LayerWindow, Lock, Media, MediaPlayer, Monitor, Pointer, Rectangle, Row, Service, Shape, SpaceBetween, Stack, Start, Text, TextInput, Weight, Widget, children, window_size};
use crate::art::Art;
use crate::config::appearance::{font, overlays, text};
use crate::config::lock::{look, show};
use crate::config::pills::layout;
use crate::glyph::{Glyph, icon};
use crate::glyph::{Glyph, glyph_shapes};
use crate::home::{self, Clock};
use crate::island::{self, CLEAR, Island, faded};
use crate::system::Power;
@@ -20,6 +22,12 @@ use crate::{control, edge, notices, parse, surface, theme};
* password. every monitor shows it, all sharing the one password field.
* `amane ipc call lock` locks. killing the shell doesn't unlock: hyprland keeps the session
* locked (README, Lock screen)
*
* it redraws for the music's progress, the clock and the password, so it's drawn cheaply:
* the backdrop is one shader (lock.wgsl) instead of a gradient vello paints over the whole
* screen on every frame, the glass is surface.rs's shader, and every icon goes into one
* canvas over it all, as each canvas is a vello pass of its own. so that where each icon
* lands is known, every part has a fixed size
*/
// what the lock screen looks up once per lock, before it shows
@@ -41,6 +49,14 @@ impl Service for Shown {
// the password field's name; every monitor's lock screen shows the same one
const FIELD: &str = "lock-password";
static SHADER: LazyLock<PathBuf> = LazyLock::new(|| surface::shader_file("lock.wgsl", include_str!("lock.wgsl")));
// the icons of one frame, drawn together into one canvas over the screen
type Icons = Vec<Box<dyn Shape>>;
// a chip's parts, how wide they are, and the icon the canvas draws at their start
type Chip = (Vec<Box<dyn Widget>>, f32, Option<(Glyph, f32, Color)>);
// `amane ipc call lock`: the splash and the picture are looked up first, so the screen shows them from its first frame
pub fn command(words: &[String]) -> String {
if !words.is_empty() {
@@ -88,75 +104,115 @@ pub fn view(_: &Monitor) -> LayerWindow {
window.child(screen(width, height))
}
fn alpha(share: f32) -> u8 {
(share.clamp(0.0, 1.0) * 255.0).round() as u8
}
// about how wide words are at `size`, as the chips size themselves
fn words_width(words: &str, size: f32) -> f32 {
words.chars().count() as f32 * size * font::letter_width()
}
// words in a box `width` wide, from its left, elided past it
fn words(words: &str, size: f32, color: Color, bold: bool, width: f32) -> Rectangle {
let text = Text::new(words).size(size).color(color).elide();
let text = if bold { text.weight(Weight::Bold) } else { text };
Rectangle::new().width(width).height(size * 1.4).align_child(Start, Center).child(text)
}
// empty room `width` wide, where an icon of the canvas goes or just space
fn room(width: f32) -> Rectangle {
Rectangle::new().width(width).height(1.0)
}
// `glyph` centred on (x, y)
fn place(icons: &mut Icons, glyph: Glyph, size: f32, color: Color, (x, y): (f32, f32)) {
icons.extend(glyph_shapes(glyph, size, color, 0.0, (x - size / 2.0, y - size / 2.0)));
}
fn screen(width: f32, height: f32) -> Stack {
let clock = Clock::read();
let shown = Shown::read();
let gap = look::gap();
let spacer = |height: f32| Rectangle::new().width(1.0).height(height);
let mut icons: Icons = Vec::new();
let upper = Column::new(children![spacer(look::top()), upper(&clock, &shown, width), middle(clock.now())]).gap(gap).align(Center);
let lower = Column::new(children![lower(&clock), spacer(look::bottom())]).align(Center);
// the top part downward from look::top, the bottom part upward from look::bottom
let upper_y = look::top() + gap;
let (upper, upper_h) = upper(&clock, &shown, width, upper_y, &mut icons);
let middle = middle(clock.now(), width, upper_y + upper_h + gap, &mut icons);
let content = Column::new(children![upper, lower]).width(width).height(height).justify(SpaceBetween).align(Center);
let lower_y = height - look::bottom() - lower_height();
let lower = lower(&clock, width, lower_y, &mut icons);
Stack::new(children![backdrop(width, height, shown.picture.as_deref()), content]).width(width).height(height)
}
let top = Column::new(children![Rectangle::new().width(1.0).height(look::top()), upper, middle]).gap(gap).align(Center);
let bottom = Column::new(children![lower, Rectangle::new().width(1.0).height(look::bottom())]).align(Center);
// the wallpaper's colours as a gradient, the picture blurred over it, and a veil for the words
fn backdrop(width: f32, height: f32, picture: Option<&str>) -> Stack {
let theme = theme::now();
let [high, middle, low] = theme.surfaces();
let content = Column::new(children![top, bottom]).width(width).height(height).justify(SpaceBetween).align(Center);
let mut layers: Vec<Box<dyn Widget>> = vec![Box::new(Rectangle::new().width(width).height(height).fill(Gradient::linear(160.0, [(0.0, high), (0.55, middle), (1.0, low)])))];
let mut layers = backdrop(width, height, shown.picture.as_deref());
if let Some(path) = picture {
let blur = look::blur() as u32;
layers.push(Box::new(content));
// blurred: a small copy, blurred once when decoded and stretched over the screen, costs nothing per frame
let image = if blur == 0 {
Image::cover(path).thumbnail(width.ceil() as u32, height.ceil() as u32)
} else {
let copy = look::copy_width() as u32;
Image::cover(path).thumbnail(copy, (copy as f32 * height / width).ceil() as u32).blurred(blur)
};
layers.push(Box::new(Rectangle::new().width(width).height(height).fill(image)));
if !icons.is_empty() {
layers.push(Box::new(Canvas::new().width(width).height(height).shapes(icons)));
}
layers.push(Box::new(Rectangle::new().width(width).height(height).fill(faded(Color::BLACK, alpha(look::dim())))));
Stack::new(layers).width(width).height(height)
}
// the date with the weather now, the clock, hyprland's splash
fn upper(clock: &Clock, shown: &Shown, width: f32) -> Column {
// the wallpaper colours' gradient, the picture blurred over it, and a veil over that darker at the edges: lock.wgsl
fn backdrop(width: f32, height: f32, picture: Option<&str>) -> Vec<Box<dyn Widget>> {
let [high, middle, low] = theme::now().surfaces();
let shade = |layer: f32| {
let values = vec![[width, height, 160.0, layer], surface::plain(high), surface::plain(middle), surface::plain(low), [look::dim(), look::vignette(), 0.0, 0.0]];
Box::new(Rectangle::new().width(width).height(height).shader(SHADER.as_path()).shader_values(values)) as Box<dyn Widget>
};
let Some(path) = picture else {
return vec![shade(0.0)];
};
let blur = look::blur() as u32;
// blurred: a small copy, blurred once when decoded and stretched over the screen, costs nothing per frame
let image = if blur == 0 {
Image::cover(path).thumbnail(width.ceil() as u32, height.ceil() as u32)
} else {
let copy = look::copy_width() as u32;
Image::cover(path).thumbnail(copy, (copy as f32 * height / width).ceil() as u32).blurred(blur)
};
// the gradient shows while the picture decodes
vec![shade(1.0), Box::new(Rectangle::new().width(width).height(height).fill(image)), shade(2.0)]
}
// the date with the weather now, the clock, hyprland's splash, from `y` down; and how tall
fn upper(clock: &Clock, shown: &Shown, width: f32, y: f32, icons: &mut Icons) -> (Rectangle, f32) {
let theme = theme::now();
let size = look::date_size();
let now = clock.now();
let inner = look::gap() * 0.4;
let mut date: Vec<Box<dyn Widget>> = Vec::new();
// the date, and the weather's icon and words after it
let date = if now > 0 { home::date_words(now) } else { String::new() };
let date_w = words_width(&date, size);
let date_h = size * 1.4;
if now > 0 {
date.push(Box::new(Text::new(home::date_words(now)).size(size).weight(Weight::Bold).color(theme.text)));
}
let mut row: Vec<Box<dyn Widget>> = vec![Box::new(words(&date, size, theme.text, true, date_w))];
let mut row_w = date_w;
if show::weather()
&& let Some((glyph, words)) = home::weather_now()
&& let Some((glyph, said)) = home::weather_now()
{
date.push(Box::new(Rectangle::new().width(size * 0.4).height(1.0)));
date.push(Box::new(icon(glyph, size * 1.15, theme.text)));
date.push(Box::new(Text::new(words).size(size).color(theme.text)));
let (space, icon, after) = (size * 0.6, size * 1.15, size * 0.3);
let said_w = words_width(&said, size);
let total = date_w + space + icon + after + said_w;
place(icons, glyph, icon, theme.text, ((width - total) / 2.0 + date_w + space + icon / 2.0, y + date_h / 2.0));
row.extend(children![room(space), room(icon), room(after), words(&said, size, theme.text, false, said_w)]);
row_w = total;
}
let seconds = now.rem_euclid(86_400);
@@ -165,43 +221,49 @@ fn upper(clock: &Clock, shown: &Shown, width: f32) -> Column {
let digits = Text::new(time).size(look::clock_size()).weight(look::clock_weight().min(1000) as u16).color(theme.text).tight();
let family = look::clock_font();
let digits = if family.trim().is_empty() { digits } else { digits.font(family.trim()) };
let clock_h = digits.height_in(width);
let mut parts: Vec<Box<dyn Widget>> = vec![Box::new(Row::new(date).gap(size * 0.4).align(Center)), Box::new(digits)];
let mut parts: Vec<Box<dyn Widget>> = vec![
Box::new(Rectangle::new().width(row_w).height(date_h).child(Row::new(row).align(Center))),
Box::new(Rectangle::new().width(width).height(clock_h).align_child(Center, Center).child(digits)),
];
let mut height = date_h + inner + clock_h;
if show::splash() && !shown.splash.is_empty() {
let size = size * 0.7;
let words = format!("“{}”", shown.splash);
let said = format!("“{}”", shown.splash);
let said_w = (words_width(&said, size) + size).min(width * 0.7);
// as wide as the words, at most most of the screen: elided text starts at its box's left
let box_w = (words_width(&words, size) + size).min(width * 0.7);
parts.push(Box::new(Rectangle::new().width(box_w).height(size * 1.6).align_child(Center, Center).child(Text::new(words).size(size).color(theme.dim).elide())));
parts.push(Box::new(words(&said, size, theme.dim, false, said_w)));
height += inner + size * 1.4;
}
Column::new(parts).gap(look::gap() * 0.4).align(Center)
(Rectangle::new().width(width).height(height).align_child(Center, Start).child(Column::new(parts).gap(inner).align(Center)), height)
}
// the music, then the chips
fn middle(now: i64) -> Column {
// the music, then the chips, from `y` down
fn middle(now: i64, width: f32, y: f32, icons: &mut Icons) -> Column {
let gap = look::gap() * 0.75;
let mut parts: Vec<Box<dyn Widget>> = Vec::new();
let mut y = y;
if show::music()
&& let Some(card) = music()
&& let Some(card) = music(width, y, icons)
{
parts.push(Box::new(card));
y += look::card_height() + gap;
}
let chips = chips(now);
if !chips.is_empty() {
parts.push(Box::new(Row::new(chips).gap(look::gap() * 0.6).align(Center)));
if let Some(chips) = chips(now, width, y, icons) {
parts.push(Box::new(chips));
}
Column::new(parts).gap(look::gap() * 0.75).align(Center)
Column::new(parts).gap(gap).align(Center)
}
// the shown player's song: its cover, title, artist and how far it is, and its buttons
fn music() -> Option<Rectangle> {
// the shown player's song at `y`: its cover, title, artist and how far it is, and its buttons
fn music(width: f32, y: f32, icons: &mut Icons) -> Option<Rectangle> {
let media = Media::read();
let island = Island::read();
let art = Art::read();
@@ -209,24 +271,30 @@ fn music() -> Option<Rectangle> {
let player = island::shown(&media, island.selected.as_deref())?;
let (width, height) = (look::card_width(), look::card_height());
let (card_w, card_h) = (look::card_width(), look::card_height());
let gap = look::gap() * 0.75;
let cover = look::cover().min(height - 8.0);
let pad = (height - cover) / 2.0;
let cover = look::cover().min(card_h - 8.0);
let pad = (card_h - cover) / 2.0;
// where the card's row starts, and the height every part of it is centred on
let left = (width - card_w) / 2.0 + pad;
let middle = y + card_h / 2.0;
let picture = Rectangle::new().width(cover).height(cover).radius(cover * 0.18);
let picture = if art.path.is_empty() {
picture.fill(theme.accent_soft).align_child(Center, Center).child(icon(Glyph::Note, cover * 0.45, theme.accent))
place(icons, Glyph::Note, cover * 0.45, theme.accent, (left + cover / 2.0, middle));
picture.fill(theme.accent_soft)
} else {
let pixels = (cover * 2.0).ceil() as u32;
picture.fill(Image::cover(art.path.clone()).thumbnail(pixels, pixels))
};
let buttons = show::controls().then(|| controls(player));
let buttons_w = buttons.as_ref().map_or(0.0, |(_, width)| width + gap);
let words_w = (width - 2.0 * pad - cover - gap - buttons_w).max(0.0);
let (small, big) = (text::title() * 1.9, text::title() * 1.9 * 1.25);
let buttons_w = if show::controls() { 2.0 * small + big + 8.0 + gap } else { 0.0 };
let words_w = (card_w - 2.0 * pad - cover - gap - buttons_w).max(0.0);
let title = if player.title().is_empty() { player.identity() } else { player.title() };
@@ -235,105 +303,131 @@ fn music() -> Option<Rectangle> {
let track = Rectangle::new().width(words_w).height(4.0).radius(Full).fill(overlays::track()).align_child(Start, Center).child(Rectangle::new().width((words_w * done).max(4.0)).height(4.0).radius(Full).fill(theme.text));
let line = |words: &str, size: f32, color: Color, bold: bool| {
let words = Text::new(words).size(size).color(color).elide();
let words = if bold { words.weight(Weight::Bold) } else { words };
let said = Column::new(children![words(title, text::title(), theme.text, true, words_w), words(player.artist(), text::body(), theme.dim, false, words_w), track]).gap(6.0);
Rectangle::new().width(words_w).height(size * 1.35).align_child(Start, Center).child(words)
};
let mut items: Vec<Box<dyn Widget>> = vec![Box::new(picture), Box::new(said)];
let words = Column::new(children![line(title, text::title(), theme.text, true), line(player.artist(), text::body(), theme.dim, false), track]).gap(6.0);
let mut items: Vec<Box<dyn Widget>> = vec![Box::new(picture), Box::new(words)];
if let Some((buttons, _)) = buttons {
items.push(Box::new(buttons));
if show::controls() {
items.push(Box::new(controls(player, (left + cover + gap + words_w + gap, middle), small, big, icons)));
}
let content = Rectangle::new().width(width).height(height).padding(pad).align_child(Start, Center).child(Row::new(items).gap(gap).align(Center));
let content = Rectangle::new().width(card_w).height(card_h).padding(pad).align_child(Start, Center).child(Row::new(items).gap(gap).align(Center));
Some(surface::shell(width, height, layout::radius().min(height / 2.0), &theme.surfaces(), 0.0, content))
Some(surface::shell(card_w, card_h, layout::radius().min(card_h / 2.0), &theme.surfaces(), 0.0, content))
}
// previous, play or pause, next, and how wide they are
fn controls(player: &MediaPlayer) -> (Row, f32) {
// previous, play or pause, next, from (x, the middle's y), their icons into the canvas
fn controls(player: &MediaPlayer, (x, y): (f32, f32), small: f32, big: f32, icons: &mut Icons) -> Row {
let theme = theme::now();
let small = text::title() * 1.9;
let big = small * 1.25;
let button = |glyph: Glyph, size: f32, fill: Color, pressed: Box<dyn Fn()>| {
Rectangle::new().width(size).height(size).radius(Full).fill(fill).align_child(Center, Center).cursor(Pointer).on_click(move |_| pressed()).child(icon(glyph, size * 0.42, theme.text))
};
let (previous, toggle, next) = (player.clone(), player.clone(), player.clone());
let middle = if player.playing() { Glyph::Pause } else { Glyph::Play };
let row = Row::new(children![
button(Glyph::Previous, small, CLEAR, Box::new(move || previous.previous())),
button(middle, big, overlays::chip(), Box::new(move || toggle.play_pause())),
button(Glyph::Next, small, CLEAR, Box::new(move || next.next())),
for (glyph, size, centre) in [(Glyph::Previous, small, x + small / 2.0), (middle, big, x + small + 4.0 + big / 2.0), (Glyph::Next, small, x + small + 4.0 + big + 4.0 + small / 2.0)] {
place(icons, glyph, size * 0.42, theme.text, (centre, y));
}
let button = |size: f32, fill: Color, pressed: Box<dyn Fn()>| Rectangle::new().width(size).height(size).radius(Full).fill(fill).cursor(Pointer).on_click(move |_| pressed());
let (previous, toggle, next) = (player.clone(), player.clone(), player.clone());
Row::new(children![
button(small, CLEAR, Box::new(move || previous.previous())),
button(big, overlays::chip(), Box::new(move || toggle.play_pause())),
button(small, CLEAR, Box::new(move || next.next())),
])
.gap(4.0)
.align(Center);
(row, 2.0 * small + big + 8.0)
.align(Center)
}
// a glass chip around `parts`, which are `inner` wide
fn chip(parts: Vec<Box<dyn Widget>>, inner: f32) -> Box<dyn Widget> {
fn chip(parts: Vec<Box<dyn Widget>>, inner: f32) -> Rectangle {
let height = look::chip_height();
let width = inner + height;
let content = Rectangle::new().width(width).height(height).align_child(Center, Center).child(Row::new(parts).gap(8.0).align(Center));
Box::new(surface::shell(width, height, height / 2.0, &theme::now().surfaces(), 0.0, content))
surface::shell(width, height, height / 2.0, &theme::now().surfaces(), 0.0, content)
}
// words in a chip, cut with an ellipsis past `most` wide; and how wide they are
fn chip_words(words: &str, most: f32) -> (Rectangle, f32) {
let size = text::body();
let width = words_width(words, size).min(most);
(Rectangle::new().width(width).height(size * 1.4).align_child(Start, Center).child(Text::new(words).size(size).color(theme::now().text).elide()), width)
}
// the next event, the new notifications, the battery: each its own chip, when there is one
fn chips(now: i64) -> Vec<Box<dyn Widget>> {
// the next event, the new notifications, the battery at `y`: each its own chip, when there is one
fn chips(now: i64, width: f32, y: f32, icons: &mut Icons) -> Option<Row> {
let theme = theme::now();
let icon_size = text::body() * 1.3;
let mut chips = Vec::new();
let (size, height) = (text::body(), look::chip_height());
let icon = size * 1.3;
let spacing = look::gap() * 0.6;
// each chip's parts and inner width, and an icon at the start of its parts
let mut wanted: Vec<Chip> = Vec::new();
if show::event()
&& let Some((when, what, color)) = home::next_event(now)
{
let said = format!("{when} · {what}");
let said_w = words_width(&said, size).min(look::card_width() * 0.8);
let dot = Rectangle::new().width(9.0).height(9.0).radius(Full).fill(color.unwrap_or(theme.accent));
let (words, width) = chip_words(&format!("{when} · {what}"), look::card_width() * 0.8);
chips.push(chip(children![dot, words], 9.0 + 8.0 + width));
wanted.push((children![dot, words(&said, size, theme.text, false, said_w)], 9.0 + 8.0 + said_w, None));
}
let unread = notices::unread();
if show::notifications() && unread > 0 {
let words = if unread == 1 { String::from("1 new notification") } else { format!("{unread} new notifications") };
let (words, width) = chip_words(&words, look::card_width());
let said = if unread == 1 { String::from("1 new notification") } else { format!("{unread} new notifications") };
let said_w = words_width(&said, size).min(look::card_width());
chips.push(chip(children![icon(Glyph::Bell, icon_size, theme.text), words], icon_size + 8.0 + width));
wanted.push((children![room(icon), words(&said, size, theme.text, false, said_w)], icon + 8.0 + said_w, Some((Glyph::Bell, icon, theme.text))));
}
if show::battery()
&& let Some(charge) = &Power::read().battery
{
chips.push(chip(children![control::battery(charge)], control::battery_width(charge)));
let bolt = (charge.status == "Charging").then_some((Glyph::Bolt, 11.0, theme.accent));
wanted.push((children![control::battery_with(charge, false)], control::battery_width(charge), bolt));
}
chips
if wanted.is_empty() {
return None;
}
let total: f32 = wanted.iter().map(|(_, inner, _)| inner + height).sum::<f32>() + spacing * (wanted.len() - 1) as f32;
let mut x = (width - total) / 2.0;
let mut row: Vec<Box<dyn Widget>> = Vec::new();
for (parts, inner, icon) in wanted {
// a chip's parts start half its height in
if let Some((glyph, size, color)) = icon {
place(icons, glyph, size, color, (x + height / 2.0 + size / 2.0, y + height / 2.0));
}
row.push(Box::new(chip(parts, inner)));
x += inner + height + spacing;
}
Some(Row::new(row).gap(spacing).align(Center))
}
// the avatar, the name, the password and what became of the last one
fn lower(clock: &Clock) -> Column {
fn name_height() -> f32 {
text::title() * 1.4
}
fn note_height() -> f32 {
text::body() * 1.6
}
// how tall the avatar, the name, the password and the note are together
fn lower_height() -> f32 {
let gap = look::gap() * 0.6;
look::avatar_size() + gap + name_height() + gap + look::field_height() + gap + note_height()
}
// the avatar, the name, the password and what became of the last one, from `y` down
fn lower(clock: &Clock, width: f32, y: f32, icons: &mut Icons) -> Column {
let theme = theme::now();
let lock = Lock::read();
let gap = look::gap() * 0.6;
let size = look::avatar_size();
let avatar = Rectangle::new().width(size).height(size).radius(Full).border(2.0, faded(theme.text, 90));
@@ -344,27 +438,32 @@ fn lower(clock: &Clock) -> Column {
avatar.fill(Image::cover(path).thumbnail(pixels, pixels))
}
None => avatar.fill(faded(theme.accent, 60)).align_child(Center, Center).child(icon(Glyph::Person, size * 0.54, theme.accent)),
None => {
place(icons, Glyph::Person, size * 0.54, theme.accent, (width / 2.0, y + size / 2.0));
avatar.fill(faded(theme.accent, 60))
}
};
let name = Text::new(home::name()).size(text::title()).weight(Weight::Bold).color(theme.text);
let name = Rectangle::new().width(width).height(name_height()).align_child(Center, Center).child(Text::new(home::name()).size(text::title()).weight(Weight::Bold).color(theme.text));
let (width, height) = (look::field_width(), look::field_height());
let size = text::popup();
let (field_w, field_h) = (look::field_width(), look::field_height());
let letters = text::popup();
let icon = letters * 1.1;
let start = field_h * 0.45;
let input = TextInput::new(FIELD).password().focused().placeholder("Enter password").size(size).color(theme.text).on_submit(submit);
let field_y = y + size + gap + name_height() + gap;
let inside = Row::new(children![
Rectangle::new().width(height * 0.45).height(1.0),
icon(Glyph::Lock, size * 1.1, theme.dim),
Rectangle::new().width(width - height * 0.9 - size * 1.1 - 10.0).height(height).align_child(Start, Center).child(input),
])
.gap(10.0)
.align(Center);
place(icons, Glyph::Lock, icon, theme.dim, ((width - field_w) / 2.0 + start + icon / 2.0, field_y + field_h / 2.0));
let input = TextInput::new(FIELD).password().focused().placeholder("Enter password").size(letters).color(theme.text).on_submit(submit);
// the lock icon's room, then the input 10 px after it
let inside = Row::new(children![room(start), room(icon), room(10.0), Rectangle::new().width(field_w - 2.0 * start - icon - 10.0).height(field_h).align_child(Start, Center).child(input)]).align(Center);
// a wrong password lights the field's rim
let lift = if lock.failed() { 1.0 } else { 0.0 };
let field = surface::shell(width, height, height / 2.0, &theme.surfaces(), lift, Rectangle::new().width(width).height(height).align_child(Start, Center).child(inside));
let field = surface::shell(field_w, field_h, field_h / 2.0, &theme.surfaces(), lift, Rectangle::new().width(field_w).height(field_h).align_child(Start, Center).child(inside));
let (note, color) = if lock.checking() {
("Checking…", theme.dim)
@@ -374,9 +473,9 @@ fn lower(clock: &Clock) -> Column {
("", theme.dim)
};
let note = Rectangle::new().width(width).height(text::body() * 1.6).align_child(Center, Center).child(Text::new(note).size(text::body()).color(color));
let note = Rectangle::new().width(field_w).height(note_height()).align_child(Center, Center).child(Text::new(note).size(text::body()).color(color));
Column::new(children![avatar, name, field, note]).gap(look::gap() * 0.6).align(Center)
Column::new(children![avatar, name, field, note]).gap(gap).align(Center)
}
// Enter: the field empties at once, pam gets the password on its own thread
+36
View File
@@ -0,0 +1,36 @@
// the lock screen's backdrop in one pass: the wallpaper colours' gradient, and over the picture
// a veil that darkens it, more toward the edges. drawn as amane's gradient it was a vello pass
// over the whole screen on every frame (vello keeps no gradient between frames). lock.rs fills
// the values:
// 0: width, height, the gradient's angle in degrees (0 runs bottom to top, 90 left to right), layer
// 1-3: the gradient's colours at 0, 0.55 and 1, plain
// 4: the veil's darkness and the vignette's strength, 0 to 1
// layer 1 is the gradient (under the picture), 2 the veil (over it), 0 both (no picture)
@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let extent = values[0].xy;
let angle = radians(values[0].z);
let layer = u32(values[0].w);
// where this pixel lies along the gradient, 0 at its start corner and 1 at its end
let direction = vec2<f32>(sin(angle), -cos(angle));
let reach = abs(direction.x) * extent.x + abs(direction.y) * extent.y;
let t = clamp(dot((uv - 0.5) * extent, direction) / max(reach, 1.0) + 0.5, 0.0, 1.0);
let gradient = select(mix(values[2], values[3], (t - 0.55) / 0.45), mix(values[1], values[2], t / 0.55), t < 0.55);
// the veil, darker toward the corners: 0 in the middle, 1 in a corner
let corner = length((uv - 0.5) * 2.0) / 1.4142;
let dark = clamp(values[4].x + (1.0 - values[4].x) * values[4].y * smoothstep(0.3, 1.0, corner), 0.0, 1.0);
if layer == 1u {
return vec4<f32>(gradient.rgb, 1.0);
}
if layer == 2u {
return vec4<f32>(0.0, 0.0, 0.0, dark);
}
return vec4<f32>(gradient.rgb * (1.0 - dark), 1.0);
}