Files
amane-config/src/workspaces.rs
T
server2 9329d250f6 workspaces: a pill top left on every monitor with its workspaces
- one window per monitor (namespace magic-workspaces, Overlay like the
  island), showing that monitor's hyprland workspaces in the island's
  look: cover gradient, pink glow
- focused workspace pink, shown-elsewhere soft pink, empty ones dim;
  hover eases in (one at a time), a click switches workspace
- special workspaces are skipped; name-only ones show their first letter
- event-driven through Workspaces: redraws only on hyprland events and
  hover
2026-10-08 02:09:25 +02:00

155 lines
5.1 KiB
Rust

use std::collections::HashMap;
use std::time::Duration;
use amane::{
Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Palette, Parent, Pointer,
Rectangle, Row, Service, Shadow, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
};
use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, DIM, EDGE, HOVER_MS, INK, PINK, SHADOW, TEXT, TOP_GAP, backdrop, backdrop_colors, faded};
// one round slot per workspace, inside the pill's inset; the pill sits LEFT px from the monitor's edge
const SLOT: f32 = 28.0;
const SLOT_GAP: f32 = 4.0;
const INSET: f32 = 5.0;
const LEFT: f32 = 12.0;
// each hovered workspace's highlight by workspace id, fading between 0 and 1
#[derive(Default)]
struct Hover(HashMap<i64, Animation>);
impl Service for Hover {
fn new() -> Self {
Self::default()
}
// changes only through input
fn listen() {}
}
/*
* a pill in the top left corner of every monitor with that monitor's hyprland workspaces,
* in the island's look (cover gradient, pink glow). the focused workspace is pink, the one
* shown on another monitor a softer pink, empty ones dim; a click switches to it.
* Workspaces follows hyprland's events, so nothing polls
*/
pub fn view(monitor: &Monitor) -> LayerWindow {
let workspaces = Workspaces::read();
let hover = Hover::read();
let palette = Palette::read();
// special workspaces (scratchpads) aren't places to switch to
let here: Vec<&Workspace> = workspaces
.list()
.iter()
.filter(|workspace| workspace.output() == Some(monitor.name.as_str()))
.filter(|workspace| !workspace.name().is_some_and(|name| name.starts_with("special")))
.collect();
let count = here.len() as f32;
let width = 2.0 * INSET + count * SLOT + (count - 1.0).max(0.0) * SLOT_GAP;
let window = LayerWindow::new()
.width(LEFT + width + SHADOW)
.height(TOP_GAP + COMPACT_H + SHADOW)
.anchor_vertical(Vertical::Top)
.anchor_horizontal(Horizontal::Left)
// Overlay like the island: on a desktop of fullscreen windows, Top is hidden behind every one of them
.layer(Layer::Overlay)
.space(Zone::Ignore)
.namespace("magic-workspaces");
if here.is_empty() {
return window.visible(false);
}
let mut slots: Vec<Box<dyn Widget>> = Vec::new();
for workspace in here {
let id = workspace.id();
let lit = hover.0.get(&id).map_or(0.0, Animation::value);
// hyprland numbers its workspaces; a name-only one shows its first letter
let label = match workspace.name() {
Some(name) if workspace.index() == 0 => name.chars().next().map_or(String::new(), |letter| letter.to_uppercase().collect()),
_ => workspace.index().to_string(),
};
let (fill, color, weight) = if workspace.focused() {
(PINK, INK, Weight::Bold)
} else if workspace.active() {
(faded(PINK, 70), PINK, Weight::Bold)
} else {
let resting = if workspace.windows() > 0 { TEXT } else { DIM };
(Color::blend(CLEAR, CHIP_HOVER, lit), Color::blend(resting, TEXT, lit), Weight::Regular)
};
slots.push(Box::new(
Rectangle::new()
.width(SLOT)
.height(SLOT)
.radius(Full)
.fill(fill)
.align_child(Center, Center)
.scale(1.0 + 0.08 * lit)
.cursor(Pointer)
.on_hover(move |inside| hovering(id, inside))
.on_click(move |_| Workspaces::focus(id))
.child(Text::new(label).size(12.0).weight(weight).color(color)),
));
}
let colors = backdrop_colors(&palette);
let pill = Rectangle::new()
.width(width)
.height(COMPACT_H)
.radius(Full)
.fill(backdrop(&colors, 255))
.border(1.0, EDGE)
.shadow(Shadow::drop(PINK).opacity(0.3).blur(22.0))
.padding(INSET)
.align_child(Start, Center)
.child(Row::new(slots).gap(SLOT_GAP).align(Center));
// clicks next to the pill reach the windows underneath
let region = InputArea {
x: LEFT as i32,
y: TOP_GAP as i32,
width: width.ceil() as i32,
height: COMPACT_H as i32,
};
window.input_region(vec![region]).child(
Rectangle::new()
.width(Parent)
.height(Parent)
.padding(Padding { top: TOP_GAP, right: 0.0, bottom: 0.0, left: LEFT })
.align_child(Start, Start)
.child(pill),
)
}
/*
* one workspace lit at a time: amane matches hover targets by their place in the frame,
* so a workspace that closes under the pointer never hears it leave
*/
fn hovering(id: i64, inside: bool) {
let mut hover = Hover::write();
if inside {
for (other, highlight) in &mut hover.0 {
if *other != id {
highlight.to(0.0);
}
}
}
hover
.0
.entry(id)
.or_insert_with(|| Animation::new(0.0).duration(Duration::from_millis(HOVER_MS)))
.to(if inside { 1.0 } else { 0.0 });
}