Files
waydock/src/style.rs
claude 1c6ed7815b
All checks were successful
check / check (push) Successful in 1m34s
Skroll-stegning i fönsterlistan + klick/launch-debugloggar
Med en grupps lista öppen stegar skroll (på gruppikonen eller över
listan) markeringen mellan raderna — markerad rad får accentbakgrund
och triggar samma förhandsvisning som hover (peek/fokus enligt
list_hover). Funkar i både hover- och klickläge; utan öppen lista
beter sig skroll som förut (cykla fönster). Watchdogen räknar
stegning som aktivitet så listan inte stängs mitt i.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 00:53:16 +02:00

215 lines
5.6 KiB
Rust

// Config → CSS. All theming (färger, transparens, skuggor, accent,
// storlekar, animationer) genereras här och laddas i en CssProvider som
// byts ut vid live-omladdning. Skärmar med per-skärm-överridar får egna
// CSS-block scopade på dockfönstrets output-klass (o-<connector>).
use gtk4 as gtk;
use gtk4::gdk;
use crate::config::{Config, IndicatorStyle};
/// CSS-klass för en viss skärms dockfönster.
pub fn output_class(connector: &str) -> String {
let safe: String = connector
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
format!("o-{safe}")
}
/// "#rrggbb" + alpha → "rgba(r, g, b, a)". Trasig färg → grå.
fn rgba(hex: &str, alpha: f64) -> String {
let h = hex.trim_start_matches('#');
let (r, g, b) = if h.len() == 6 {
(
u8::from_str_radix(&h[0..2], 16).unwrap_or(0x88),
u8::from_str_radix(&h[2..4], 16).unwrap_or(0x88),
u8::from_str_radix(&h[4..6], 16).unwrap_or(0x88),
)
} else {
(0x88, 0x88, 0x88)
};
format!("rgba({r}, {g}, {b}, {alpha:.3})")
}
/// De utseende-styrda reglerna, med valfritt selektor-prefix
/// ("" globalt, "window.o-DP-1 " för en enskild skärm).
fn css_body(cfg: &Config, p: &str) -> String {
let a = &cfg.appearance;
let bg = rgba(&a.background, a.opacity);
let border = rgba(&a.border.color, a.border.opacity);
let accent = rgba(&a.accent, 1.0);
let accent_soft = rgba(&a.accent, 0.28);
let shadow = if a.shadow.enabled {
format!(
"box-shadow: 0 2px {}px {};",
a.shadow.blur,
rgba("#000000", a.shadow.opacity)
)
} else {
String::new()
};
let transition = if a.animations {
"transition: all 150ms ease;"
} else {
"transition: none;"
};
let hover_zoom = if a.hover_zoom { "-gtk-icon-transform: scale(1.15);" } else { "" };
let item_pad = (a.icon_spacing / 2).max(0);
let (ind_w, ind_h, ind_radius) = match a.indicator.style {
IndicatorStyle::Dot => (5, 5, 3),
IndicatorStyle::Line => (16, 3, 2),
IndicatorStyle::None => (0, 0, 0),
};
format!(
r#"
{p}.dock-panel {{
background-color: {bg};
border-radius: {radius}px;
border: {bwidth}px solid {border};
padding: {padding}px;
{shadow}
}}
{p}.dock-item {{
background: transparent;
border: none;
border-radius: {item_radius}px;
padding: 4px;
margin: 0 {item_pad}px;
min-width: {icon}px;
min-height: {icon}px;
{transition}
}}
{p}.dock-item:hover {{
background-color: {accent_soft};
}}
{p}.dock-item:hover image {{
{hover_zoom}
}}
{p}.dock-item image {{
{transition}
}}
{p}.dock-item.urgent {{
background-color: {accent_soft};
animation: waydock-pulse 900ms ease-in-out infinite alternate;
}}
{p}.indicator-box {{
min-height: {ind_h}px;
}}
{p}.indicator {{
background-color: {accent};
border-radius: {ind_radius}px;
min-width: {ind_w}px;
min-height: {ind_h}px;
margin: 0 1px;
}}
{p}.indicator.inactive {{
background-color: {accent_dim};
}}
{p}.group-badge {{
background-color: {accent};
color: #ffffff;
border-radius: 8px;
font-size: 9px;
font-weight: bold;
min-width: 14px;
min-height: 14px;
padding: 0 2px;
}}
{p}popover.waydock-menu contents {{
background-color: {menu_bg};
border-radius: 10px;
border: 1px solid {border};
}}
{p}popover.waydock-menu modelbutton,
{p}popover.waydock-menu button.flat {{
color: #eeeeee;
border-radius: 6px;
padding: 6px 10px;
}}
{p}popover.waydock-menu modelbutton:hover,
{p}popover.waydock-menu button.flat:hover,
{p}popover.waydock-menu button.flat.selected {{
background-color: {accent_soft};
}}
"#,
p = p,
bg = bg,
radius = a.corner_radius,
bwidth = a.border.width,
border = border,
padding = a.padding,
shadow = shadow,
item_radius = (a.corner_radius as i32 - 4).max(4),
item_pad = item_pad,
icon = a.icon_size + 8,
transition = transition,
hover_zoom = hover_zoom,
accent = accent,
accent_soft = accent_soft,
accent_dim = rgba(&a.accent, 0.45),
ind_h = ind_h,
ind_w = ind_w,
ind_radius = ind_radius,
menu_bg = rgba(&a.background, (a.opacity + 0.2).min(1.0)),
)
}
pub fn build_css(cfg: &Config) -> String {
let mut css = String::from(
r#"
window.waydock,
window.waydock-sensor,
window.waydock-hotspot {
background: transparent;
}
window.waydock-sensor box,
window.waydock-hotspot box {
background-color: rgba(0, 0, 0, 0.004);
}
/* utan "to" animeras tillbaka till regelns egen bakgrund (accenten) */
@keyframes waydock-pulse {
from { background-color: transparent; }
}
"#,
);
css += &css_body(cfg, "");
// skärmar med överridar får egna, mer specifika block
for name in cfg.outputs.keys() {
let eff = cfg.for_output(name);
css += &css_body(&eff, &format!("window.{} ", output_class(name)));
}
css
}
/// Ladda (eller byt ut) den globala CSS-providern.
pub fn apply(cfg: &Config, previous: Option<&gtk::CssProvider>) -> gtk::CssProvider {
let display = gdk::Display::default().expect("ingen display");
if let Some(old) = previous {
gtk::style_context_remove_provider_for_display(&display, old);
}
let provider = gtk::CssProvider::new();
provider.load_from_string(&build_css(cfg));
gtk::style_context_add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
provider
}