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
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user