bar: implement glass-workbench's three module variants (Phase 5)
Wires modules.workspaces.style/modules.clock.style into the bar for real (Phase 3 shipped the schema but only ever consumed trail/flip), and adds cpu/ram as bar modules, so glass-workbench (bread-theme) renders correctly while liquid-motion's default path is untouched: - workspaces: "pill" never calls WorkspaceTrail::place/stretch (the trail overlay stays invisible) and honours modules.workspaces.show_empty for real, rendering unoccupied non-active workspaces dimmed via CSS instead of filtering them out of the row. "trail" keeps the exact pre-existing filter/place/stretch behaviour regardless of show_empty. - clock: "plain" registers a plain date+time label (date_lbl, built but never parented until now, plus a new clock_plain_lbl) instead of the per-digit flip box. "flip" is untouched. - cpu/ram: two new bar chips (bar_cpu_pair/bar_ram_pair, bar_cpu_lbl/ bar_ram_lbl) fed by the same AppInput::StatsUpdate data as the control panel's cpu_pair/mem_pair. Separate instances rather than reparenting the panel's own pair — a GTK widget can only have one parent, and reparenting would pull them out of the control panel's sys-grid, which no theme asked to change. theme.rs::load_css() now branches on tokens.bar_border() (full-border island vs. flush bar's bottom-only hairline) and modules.workspaces.style (dimmed/translucent trail-fill buttons vs. solid-accent-fill pills), and unconditionally gains .clock-plain/.clock-plain-time rules. The trail/flip branches reproduce today's CSS byte-for-byte. Verification (bread-capture, isolated headless-Sway harness): - Noise floor: same baseline binary against itself varies from AE=0 (same capture minute) up to ~1900px (an in-flight digit-flip/clock-tick or a live Wi-Fi scan straddling two captures) and ~39000px on control-panel (live hardware sensors) — all pre-existing, not introduced by this change. - Regression: this binary vs. the pre-Phase-5 baseline, captured in the same clock-minute to remove the dominant noise source, is AE=0 (bit-for- bit identical) on the bar view under the default liquid-motion theme. - glass-workbench (BREAD_SHELL_THEME=glass-workbench): bar view is 1920x36 (flush, no margin) vs. liquid-motion's 1920x56 (12px margin + 44px island), square corners vs. liquid-motion's rounded island, plain "Mon 24/08 23:24" clock, and cpu/ram/wifi/battery/control chips with no media widget — matches demo 02. The isolated Sway capture harness has no live Hyprland IPC, so it reports zero workspaces under both themes (pre-existing, unrelated to this change) — the pill fill/dim CSS itself is exercised by code review and the trail-untouched regression result, not by a captured pixel with visible buttons. cargo test: 18/18 passing (breadbar), no new clippy/cargo-check warnings beyond the two pre-existing ones this task named up front.
This commit is contained in:
parent
8d80a05d9b
commit
df6b568cc1
3 changed files with 213 additions and 28 deletions
|
|
@ -18,6 +18,19 @@ pub fn current() -> String {
|
|||
format!("{} {}", date(), time())
|
||||
}
|
||||
|
||||
/// `modules.clock.format` rendered against GLib's own `DateTime::format`
|
||||
/// (a strftime subset — `%H`, `%M`, `%a`, `%d`, `%m`, ... all work). Falls
|
||||
/// back to [`time`]'s hardcoded "HH:MM" on a malformed format string rather
|
||||
/// than propagating an error — a broken theme's clock format must degrade,
|
||||
/// not crash the bar, same as every other "malformed theme" fallback in
|
||||
/// this system.
|
||||
pub fn formatted(format: &str) -> String {
|
||||
now()
|
||||
.format(format)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|_| time())
|
||||
}
|
||||
|
||||
pub fn spawn_ticker(sender: ComponentSender<App>) {
|
||||
relm4::spawn(async move {
|
||||
loop {
|
||||
|
|
|
|||
143
src/main.rs
143
src/main.rs
|
|
@ -13,7 +13,7 @@ mod surface;
|
|||
mod theme;
|
||||
mod widgets;
|
||||
|
||||
use bread_theme::shell::{Exclusive, Keyboard};
|
||||
use bread_theme::shell::{ClockStyle, Exclusive, Keyboard, WorkspaceStyle};
|
||||
use gtk4::prelude::*;
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
use hyprland::data::Workspace;
|
||||
|
|
@ -45,6 +45,12 @@ pub struct App {
|
|||
time_str: String,
|
||||
clock_digits: Vec<gtk4::Label>,
|
||||
date_lbl: gtk4::Label,
|
||||
// `modules.clock.style = "plain"` (glass-workbench, Phase 5): a plain
|
||||
// "HH:MM" label with no per-digit flip. Built alongside `clock_digits`
|
||||
// regardless of the active theme's style so switching styles needs no
|
||||
// recompile; only one of the two ever lands in a `[bar.slots]` module
|
||||
// registration (see the "Assemble" section).
|
||||
clock_plain_lbl: gtk4::Label,
|
||||
|
||||
// ── Stats bar ─────────────────────────────────────────────────────────
|
||||
// Island chrome matches the Liquid Motion demo: volume / wifi / battery
|
||||
|
|
@ -58,6 +64,14 @@ pub struct App {
|
|||
cpu_lbl: gtk4::Label,
|
||||
mem_lbl: gtk4::Label,
|
||||
pwr_lbl: gtk4::Label,
|
||||
// `[bar.slots].right = [..., "cpu", "ram", ...]` (glass-workbench, Phase
|
||||
// 5): separate instances from `cpu_pair`/`mem_pair` above, which stay
|
||||
// parented in the control panel's sys-grid — a GTK widget can only have
|
||||
// one parent, so reusing those here would mean reparenting them out of
|
||||
// the panel, changing panel behaviour no theme asked to change. Fed by
|
||||
// the same `AppInput::StatsUpdate` data.
|
||||
bar_cpu_lbl: gtk4::Label,
|
||||
bar_ram_lbl: gtk4::Label,
|
||||
gpu_lbl: gtk4::Label,
|
||||
vol_lbl: gtk4::Label,
|
||||
bat_lbl: gtk4::Label,
|
||||
|
|
@ -388,6 +402,22 @@ impl SimpleComponent for App {
|
|||
date_lbl.add_css_class("date-label");
|
||||
date_lbl.set_visible(false);
|
||||
|
||||
// `modules.clock.style = "plain"` (glass-workbench): date_lbl above
|
||||
// plus one plain "HH:MM" label, no per-digit flip markup at all.
|
||||
// Built unconditionally alongside the flip clock so a theme's style
|
||||
// choice is just which of the two gets registered into the "clock"
|
||||
// slot below — see "Assemble".
|
||||
let clock_plain_lbl = gtk4::Label::new(Some(&bar::clock::time()));
|
||||
clock_plain_lbl.add_css_class("clock-plain-time");
|
||||
clock_plain_lbl.set_valign(gtk4::Align::Center);
|
||||
clock_plain_lbl.set_vexpand(false);
|
||||
let clock_plain_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||
clock_plain_box.add_css_class("clock-plain");
|
||||
clock_plain_box.set_valign(gtk4::Align::Center);
|
||||
clock_plain_box.set_vexpand(false);
|
||||
clock_plain_box.append(&date_lbl);
|
||||
clock_plain_box.append(&clock_plain_lbl);
|
||||
|
||||
// Center area: [media_widget · widgets · clock · widgets]
|
||||
let center_area = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
|
||||
center_area.add_css_class("center-area");
|
||||
|
|
@ -420,6 +450,18 @@ impl SimpleComponent for App {
|
|||
pair.set_hexpand(true);
|
||||
}
|
||||
gpu_pair.set_visible(false);
|
||||
|
||||
// `[bar.slots].right = [..., "cpu", "ram", ...]` (glass-workbench,
|
||||
// Phase 5): separate chip instances from `cpu_pair`/`mem_pair`
|
||||
// above, which stay parented in the control panel's sys-grid below
|
||||
// — reusing them here would mean reparenting them out of the panel.
|
||||
// Same icons, same `.stat-pair` chip styling every other bar chip
|
||||
// (volume/battery) already uses; fed by the same `StatsUpdate` data.
|
||||
let bar_cpu_lbl = stat_label();
|
||||
let bar_ram_lbl = stat_label();
|
||||
let bar_cpu_pair = stat_pair(asset!("CPU.svg"), &bar_cpu_lbl);
|
||||
let bar_ram_pair = stat_pair(asset!("RAM Usage.svg"), &bar_ram_lbl);
|
||||
|
||||
let system_stats_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
||||
system_stats_box.add_css_class("sys-grid");
|
||||
let sys_row1 = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||
|
|
@ -721,11 +763,32 @@ impl SimpleComponent for App {
|
|||
let mut bar_modules = bar::slots::ModuleRegistry::new();
|
||||
bar_modules.register("workspaces", &workspace_trail.overlay);
|
||||
bar_modules.register("media", &media_widget);
|
||||
bar_modules.register("clock", &clock_box);
|
||||
// `modules.clock.style`: "flip" (default, liquid-motion) registers
|
||||
// the existing per-digit clock_box unchanged; "plain" (glass-
|
||||
// workbench) registers clock_plain_box instead and reveals date_lbl
|
||||
// per `show_date` — clock_box/clock_digits are still fully built in
|
||||
// that case, just never placed in any slot. "none" (Phase 6+,
|
||||
// unused today) registers neither.
|
||||
match bar_shell_theme.modules().clock.style {
|
||||
ClockStyle::Plain => {
|
||||
date_lbl.set_visible(bar_shell_theme.modules().clock.show_date);
|
||||
bar_modules.register("clock", &clock_plain_box);
|
||||
}
|
||||
ClockStyle::Flip => bar_modules.register("clock", &clock_box),
|
||||
ClockStyle::None => {}
|
||||
}
|
||||
bar_modules.register("volume", &vol_box);
|
||||
bar_modules.register("wifi", &connectivity_pair);
|
||||
bar_modules.register("battery", &bat_box);
|
||||
bar_modules.register("control", &hamburger_btn);
|
||||
// `[bar.slots].right = [..., "cpu", "ram", ...]` (glass-workbench):
|
||||
// registered unconditionally, same as every other module — a theme
|
||||
// that never names "cpu"/"ram" in a slot (liquid-motion) just never
|
||||
// walks these entries in `for_each_in_slot` below, so they stay
|
||||
// built but unparented, exactly like `media_widget` does for
|
||||
// glass-workbench (which omits "media" entirely).
|
||||
bar_modules.register("cpu", &bar_cpu_pair);
|
||||
bar_modules.register("ram", &bar_ram_pair);
|
||||
|
||||
// `tray` never appears in a bar slot — it stays inside the
|
||||
// control-panel popover (built above, next to the SNI tray) — but
|
||||
|
|
@ -791,6 +854,7 @@ impl SimpleComponent for App {
|
|||
time_str: bar::clock::current(),
|
||||
clock_digits,
|
||||
date_lbl,
|
||||
clock_plain_lbl,
|
||||
system_stats_box,
|
||||
system_sep,
|
||||
cpu_pair,
|
||||
|
|
@ -800,6 +864,8 @@ impl SimpleComponent for App {
|
|||
cpu_lbl,
|
||||
mem_lbl,
|
||||
pwr_lbl,
|
||||
bar_cpu_lbl,
|
||||
bar_ram_lbl,
|
||||
gpu_lbl,
|
||||
vol_lbl,
|
||||
bat_lbl,
|
||||
|
|
@ -938,7 +1004,14 @@ impl SimpleComponent for App {
|
|||
self.active_ws = new_active;
|
||||
if let Some(btn) = self.button_map.get(&self.active_ws).cloned() {
|
||||
btn.add_css_class("active");
|
||||
self.workspace_trail.stretch(from.as_ref(), &btn);
|
||||
// Trail style only: pill/dots never call place()/
|
||||
// stretch() at all — the "active" CSS class above is
|
||||
// the whole of their active-workspace treatment
|
||||
// (solid accent fill, no trail overlay).
|
||||
if theme::shell_theme().modules().workspaces.style == WorkspaceStyle::Trail
|
||||
{
|
||||
self.workspace_trail.stretch(from.as_ref(), &btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -958,8 +1031,25 @@ impl SimpleComponent for App {
|
|||
}
|
||||
AppInput::ClockTick => {
|
||||
self.time_str = bar::clock::current();
|
||||
flip_clock_digits(&self.clock_digits, &bar::clock::time());
|
||||
self.date_lbl.set_label(&bar::clock::date());
|
||||
let clock_module = theme::shell_theme().modules().clock.clone();
|
||||
match clock_module.style {
|
||||
// Plain (glass-workbench): one label, no flip animation
|
||||
// — `flip_clock_digits` would just be wasted work (and
|
||||
// a pointless 450ms `play_once` timer) on digits that
|
||||
// are never on screen.
|
||||
ClockStyle::Plain => {
|
||||
self.clock_plain_lbl
|
||||
.set_label(&bar::clock::formatted(&clock_module.format));
|
||||
}
|
||||
// Flip (default, liquid-motion) and None both keep
|
||||
// exactly today's per-digit-flip update — None has no
|
||||
// module in a slot to display it, but there's no reason
|
||||
// to special-case skipping the (cheap, idempotent) work.
|
||||
ClockStyle::Flip | ClockStyle::None => {
|
||||
flip_clock_digits(&self.clock_digits, &bar::clock::time());
|
||||
}
|
||||
}
|
||||
}
|
||||
AppInput::StatsUpdate(stats) => {
|
||||
let cpu = match stats.cpu_temp {
|
||||
|
|
@ -969,6 +1059,11 @@ impl SimpleComponent for App {
|
|||
self.cpu_lbl.set_label(&cpu);
|
||||
self.mem_lbl.set_label(&stats.mem);
|
||||
self.pwr_lbl.set_label(&stats.power);
|
||||
// `[bar.slots].right = [..., "cpu", "ram", ...]` (glass-
|
||||
// workbench): same formatted text, separate chip instances
|
||||
// (see the App struct field docs for why).
|
||||
self.bar_cpu_lbl.set_label(&cpu);
|
||||
self.bar_ram_lbl.set_label(&stats.mem);
|
||||
match stats.gpu_usage {
|
||||
Some(g) => {
|
||||
let gpu = match stats.gpu_temp {
|
||||
|
|
@ -1306,14 +1401,33 @@ impl App {
|
|||
self.workspace_box.remove(&child);
|
||||
}
|
||||
self.button_map.clear();
|
||||
let modules = theme::shell_theme().modules().clone();
|
||||
let ws_style = modules.workspaces.style;
|
||||
let show_empty = modules.workspaces.show_empty;
|
||||
for ws in &self.workspaces {
|
||||
if ws.monitor != self.monitor {
|
||||
continue;
|
||||
}
|
||||
// Persistent empty Hyprland workspaces stay off the bar unless
|
||||
// this output is actually looking at them.
|
||||
if ws.windows == 0 && ws.id != self.active_ws {
|
||||
continue;
|
||||
let empty = ws.windows == 0 && ws.id != self.active_ws;
|
||||
if empty {
|
||||
match ws_style {
|
||||
// Trail (default, liquid-motion): unconditionally off
|
||||
// the bar, exactly as before this change — regardless
|
||||
// of `show_empty`, which liquid-motion's own manifest
|
||||
// declares `true` but this style has never consumed.
|
||||
// Changing that now would be a real, undesired
|
||||
// liquid-motion regression, not a Phase 5 fix.
|
||||
WorkspaceStyle::Trail => continue,
|
||||
// Pill/Dots: honour `show_empty` for real — demo 02's
|
||||
// pills render an unoccupied, non-active workspace at
|
||||
// reduced opacity via the `.workspace-btn:not(.occupied)
|
||||
// :not(.active)` CSS rule rather than hiding it.
|
||||
_ => {
|
||||
if !show_empty {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let btn = bar::workspaces::make_button(ws.id, &ws.name, self.active_ws, ws.windows > 0);
|
||||
if !prev.contains(&ws.id) {
|
||||
|
|
@ -1322,10 +1436,15 @@ impl App {
|
|||
self.workspace_box.append(&btn);
|
||||
self.button_map.insert(ws.id, btn);
|
||||
}
|
||||
match self.button_map.get(&self.active_ws).cloned() {
|
||||
Some(btn) if animate => self.workspace_trail.stretch(None, &btn),
|
||||
Some(btn) => self.workspace_trail.place(&btn),
|
||||
None => self.workspace_trail.clear(),
|
||||
// Trail style only: pill/dots never call place()/stretch()/clear()
|
||||
// at all — the "active" CSS class `make_button` already applies is
|
||||
// the whole of their active-workspace treatment.
|
||||
if ws_style == WorkspaceStyle::Trail {
|
||||
match self.button_map.get(&self.active_ws).cloned() {
|
||||
Some(btn) if animate => self.workspace_trail.stretch(None, &btn),
|
||||
Some(btn) => self.workspace_trail.place(&btn),
|
||||
None => self.workspace_trail.clear(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
85
src/theme.rs
85
src/theme.rs
|
|
@ -69,6 +69,66 @@ fn load_css() -> String {
|
|||
let spring = tokens.spring();
|
||||
let spring_settle = tokens.spring_settle();
|
||||
|
||||
// `tokens.bar_border()` (plan §11 Phase 5): "full" (default, liquid-
|
||||
// motion's floating island) draws a border on all four edges; "bottom"
|
||||
// (glass-workbench's flush edge-to-edge bar) draws only the hairline
|
||||
// the demo's `.bar { border-bottom: 1px solid #ffffff12 }` calls for —
|
||||
// a full border on a bar flush against the screen's top/left/right
|
||||
// edges would otherwise show as a stray line along those edges an
|
||||
// island never has to worry about. Reused below for the centerbox's
|
||||
// horizontal padding too: the flush bar's demo padding (`0 12px`,
|
||||
// symmetric) differs from the island's own asymmetric `0 8px 0 6px`.
|
||||
let flush = tokens.bar_border() == "bottom";
|
||||
let window_border = if flush {
|
||||
"border: none; border-bottom: 1px solid alpha(@on-bg, 0.07);".to_string()
|
||||
} else {
|
||||
"border: 1px solid alpha(@on-bg, 0.08);".to_string()
|
||||
};
|
||||
let centerbox_padding = if flush { "0 12px" } else { "0 8px 0 6px" };
|
||||
|
||||
// `modules.workspaces.style` (plan §11 Phase 5): "trail" (default,
|
||||
// liquid-motion) is exactly today's CSS, unchanged byte-for-byte —
|
||||
// dimmed/translucent buttons with the gradient trail overlay supplying
|
||||
// the active fill. "pill"/"dots" (glass-workbench, Phase 6+) render the
|
||||
// active state as a solid accent fill on the button itself instead,
|
||||
// since neither style ever calls `WorkspaceTrail::place`/`stretch`
|
||||
// (see `App::rebuild_buttons`) — the trail's own `.workspace-trail`
|
||||
// pill CSS is therefore irrelevant for them (it's never made visible).
|
||||
let workspace_css = match theme.modules().workspaces.style {
|
||||
bread_theme::shell::WorkspaceStyle::Trail => format!(
|
||||
".workspace-trail {{ background-image: linear-gradient(90deg, @accent, @teal);\
|
||||
background-color: @accent; border-radius: 12px; }}\
|
||||
.workspace-btn {{ background: transparent; opacity: 0.36; color: @on-bg;\
|
||||
border-radius: 12px; border: none; outline: none; box-shadow: none;\
|
||||
min-width: 28px; min-height: 28px; margin: 0; padding: 0 7px;\
|
||||
font-size: 22px; font-weight: bold;\
|
||||
transition: opacity 0.22s {spring_settle},\
|
||||
background-color 0.22s {spring_settle}; }}\
|
||||
.workspace-btn:hover {{ opacity: 0.85; background: alpha(@on-bg, 0.08); }}\
|
||||
.workspace-btn.occupied {{ opacity: 0.78; }}\
|
||||
.workspace-btn.active {{ background: transparent; color: @on-accent; opacity: 1; }}\
|
||||
.workspace-btn.active:hover {{ background: transparent; }}\
|
||||
.workspace-btn.ws-in {{ animation: row-in 0.32s {spring_settle} both; }}",
|
||||
),
|
||||
_ => {
|
||||
let accent = theme.tokens().accent_from();
|
||||
format!(
|
||||
".workspace-btn {{ background: transparent; opacity: 1; color: alpha(@on-bg, 0.4);\
|
||||
border-radius: {radius_sm}; border: none; outline: none; box-shadow: none;\
|
||||
min-width: 22px; min-height: 20px; margin: 0; padding: 0 6px;\
|
||||
font-size: 12px; font-weight: 600;\
|
||||
transition: background-color 0.22s {spring_settle},\
|
||||
color 0.22s {spring_settle}, opacity 0.22s {spring_settle}; }}\
|
||||
.workspace-btn:hover {{ background: alpha(@on-bg, 0.08); }}\
|
||||
.workspace-btn.occupied {{ color: alpha(@on-bg, 0.8); }}\
|
||||
.workspace-btn:not(.occupied):not(.active) {{ opacity: 0.35; }}\
|
||||
.workspace-btn.active {{ background: @{accent}; color: @on-accent; opacity: 1; }}\
|
||||
.workspace-btn.active:hover {{ background: @{accent}; }}\
|
||||
.workspace-btn.ws-in {{ animation: row-in 0.32s {spring_settle} both; }}",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
format!(
|
||||
"@keyframes notif-in {{ from {{ opacity: 0; margin-right: -16px; }} }}\
|
||||
@keyframes osd-in {{ from {{ opacity: 0; margin-bottom: -8px; }} }}\
|
||||
|
|
@ -79,22 +139,10 @@ fn load_css() -> String {
|
|||
@keyframes digit-flip {{ from {{ opacity: 0; margin-top: 7px; }} to {{ opacity: 1; margin-top: 0; }} }}\
|
||||
@keyframes caret-draw {{ from {{ margin-right: 200px; opacity: 0.2; }} to {{ margin-right: 4px; opacity: 1; }} }}\
|
||||
window.breadbar {{ background-color: alpha(@bg, 0.72); color: @on-bg;\
|
||||
border-radius: {radius_bar}; border: 1px solid alpha(@on-bg, 0.08); }}\
|
||||
window.breadbar > centerbox {{ padding: 0 8px 0 6px; }}\
|
||||
border-radius: {radius_bar}; {window_border} }}\
|
||||
window.breadbar > centerbox {{ padding: {centerbox_padding}; }}\
|
||||
window.breadbar button {{ min-height: 0; min-width: 0; }}\
|
||||
.workspace-trail {{ background-image: linear-gradient(90deg, @accent, @teal);\
|
||||
background-color: @accent; border-radius: 12px; }}\
|
||||
.workspace-btn {{ background: transparent; opacity: 0.36; color: @on-bg;\
|
||||
border-radius: 12px; border: none; outline: none; box-shadow: none;\
|
||||
min-width: 28px; min-height: 28px; margin: 0; padding: 0 7px;\
|
||||
font-size: 22px; font-weight: bold;\
|
||||
transition: opacity 0.22s {spring_settle},\
|
||||
background-color 0.22s {spring_settle}; }}\
|
||||
.workspace-btn:hover {{ opacity: 0.85; background: alpha(@on-bg, 0.08); }}\
|
||||
.workspace-btn.occupied {{ opacity: 0.78; }}\
|
||||
.workspace-btn.active {{ background: transparent; color: @on-accent; opacity: 1; }}\
|
||||
.workspace-btn.active:hover {{ background: transparent; }}\
|
||||
.workspace-btn.ws-in {{ animation: row-in 0.32s {spring_settle} both; }}\
|
||||
{workspace_css}\
|
||||
.clock-box {{ padding: 0 4px; }}\
|
||||
.clock-label {{ font-size: 24px; font-weight: bold; letter-spacing: 0.04em;\
|
||||
min-height: 0; padding: 0; margin-top: 3px; }}\
|
||||
|
|
@ -102,7 +150,9 @@ fn load_css() -> String {
|
|||
min-width: 15px; min-height: 0; padding: 0; margin: 0; }}\
|
||||
.clock-colon {{ min-width: 10px; opacity: 0.7; }}\
|
||||
.clock-digit.flip {{ animation: digit-flip 0.45s {spring} both; }}\
|
||||
.date-label {{ font-size: 14px; opacity: 0.52; letter-spacing: 0.04em; }}\
|
||||
.clock-plain {{ padding: 0 4px; }}\
|
||||
.clock-plain-time {{ font-size: 15px; font-weight: 600; letter-spacing: 0.04em; }}\
|
||||
.date-label {{ font-size: 12px; opacity: 0.48; letter-spacing: 0.04em; }}\
|
||||
.stat-label {{ font-size: 14px; letter-spacing: 0.02em; opacity: 0.92; }}\
|
||||
.stat-label.tick {{ animation: digit-flip 0.35s {spring} both; }}\
|
||||
.stats-box {{ margin-right: 0; }}\
|
||||
|
|
@ -321,6 +371,9 @@ fn load_css() -> String {
|
|||
pad = pad,
|
||||
spring = spring,
|
||||
spring_settle = spring_settle,
|
||||
window_border = window_border,
|
||||
centerbox_padding = centerbox_padding,
|
||||
workspace_css = workspace_css,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue