|
|
|
@@ -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,38 +104,74 @@ 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);
|
|
|
|
|
|
|
|
|
|
let content = Column::new(children![top, bottom]).width(width).height(height).justify(SpaceBetween).align(Center);
|
|
|
|
|
|
|
|
|
|
let mut layers = backdrop(width, height, shown.picture.as_deref());
|
|
|
|
|
|
|
|
|
|
layers.push(Box::new(content));
|
|
|
|
|
|
|
|
|
|
if !icons.is_empty() {
|
|
|
|
|
layers.push(Box::new(Canvas::new().width(width).height(height).shapes(icons)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Stack::new(layers).width(width).height(height)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
// 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 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 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)];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(path) = picture {
|
|
|
|
|
let blur = look::blur() as u32;
|
|
|
|
|
|
|
|
|
|
// blurred: a small copy, blurred once when decoded and stretched over the screen, costs nothing per frame
|
|
|
|
@@ -131,32 +183,36 @@ fn backdrop(width: f32, height: f32, picture: Option<&str>) -> Stack {
|
|
|
|
|
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)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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
|
|
|
|
|
fn upper(clock: &Clock, shown: &Shown, width: f32) -> Column {
|
|
|
|
|
// 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
|
|
|
|
|