9329d250f6
- 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
155 lines
5.1 KiB
Rust
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 });
|
|
}
|