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:
+14
-14
@@ -25,7 +25,7 @@ const PILL_GONE: f32 = 0.18;
|
||||
const SHAPE_DONE: f32 = 0.6;
|
||||
|
||||
// hover highlights ease in and out over this long; the pill widens by PILL_HOVER_GROW
|
||||
const HOVER_MS: u64 = 220;
|
||||
pub const HOVER_MS: u64 = 220;
|
||||
const PILL_HOVER_GROW: f32 = 10.0;
|
||||
|
||||
// how much thicker, in px, the transport icons get when hovered
|
||||
@@ -33,14 +33,14 @@ const BOLD: f32 = 2.0;
|
||||
|
||||
// sizes, in pixels
|
||||
const COMPACT_W: f32 = 220.0;
|
||||
const COMPACT_H: f32 = 38.0;
|
||||
pub const COMPACT_H: f32 = 38.0;
|
||||
const WIDE_W: f32 = 560.0;
|
||||
const WIDE_RADIUS: f32 = 28.0;
|
||||
const PAD: f32 = 22.0;
|
||||
const INNER_W: f32 = WIDE_W - 2.0 * PAD;
|
||||
const GAP: f32 = 16.0;
|
||||
const TOP_GAP: f32 = 6.0;
|
||||
const SHADOW: f32 = 34.0;
|
||||
pub const TOP_GAP: f32 = 6.0;
|
||||
pub const SHADOW: f32 = 34.0;
|
||||
|
||||
// the cd: its box, the cover, the hole, and how far the candles reach past it
|
||||
const DISC_BOX: f32 = 204.0;
|
||||
@@ -91,18 +91,18 @@ const THUMB: u32 = 256;
|
||||
// Fira Code renders about 0.64 em per letter here, so a chip can be sized to its label (plus slack)
|
||||
const LETTER: f32 = 0.66;
|
||||
|
||||
const PINK: Color = Color::rgb(247, 168, 200);
|
||||
pub const PINK: Color = Color::rgb(247, 168, 200);
|
||||
const PINK_DEEP: Color = Color::rgb(150, 82, 122);
|
||||
const INK: Color = Color::rgb(40, 18, 32);
|
||||
const TEXT: Color = Color::rgb(241, 236, 246);
|
||||
const DIM: Color = Color::rgba(205, 192, 218, 160);
|
||||
pub const INK: Color = Color::rgb(40, 18, 32);
|
||||
pub const TEXT: Color = Color::rgb(241, 236, 246);
|
||||
pub const DIM: Color = Color::rgba(205, 192, 218, 160);
|
||||
const CHIP: Color = Color::rgba(255, 255, 255, 18);
|
||||
const CHIP_OPEN: Color = Color::rgba(255, 255, 255, 34);
|
||||
// a hovered chip or button; CLEAR is its starting point where there is no fill, white so the fade stays grey-free
|
||||
const CHIP_HOVER: Color = Color::rgba(255, 255, 255, 46);
|
||||
const CLEAR: Color = Color::rgba(255, 255, 255, 0);
|
||||
pub const CHIP_HOVER: Color = Color::rgba(255, 255, 255, 46);
|
||||
pub const CLEAR: Color = Color::rgba(255, 255, 255, 0);
|
||||
const TRACK: Color = Color::rgba(255, 255, 255, 24);
|
||||
const EDGE: Color = Color::rgba(255, 255, 255, 26);
|
||||
pub const EDGE: Color = Color::rgba(255, 255, 255, 26);
|
||||
const SURFACE_BOTTOM: Color = Color::rgb(13, 10, 18);
|
||||
const MENU_BG: Color = Color::rgb(35, 26, 44);
|
||||
|
||||
@@ -551,7 +551,7 @@ fn veiled(face: Rectangle, width: f32, height: f32, radius: f32, shown: f32, col
|
||||
}
|
||||
|
||||
// the cover's colours for the backdrop: most common, second most common, darkest
|
||||
fn backdrop_colors(palette: &Palette) -> [Color; 3] {
|
||||
pub fn backdrop_colors(palette: &Palette) -> [Color; 3] {
|
||||
let first = palette.dominant();
|
||||
let second = palette.colors().get(1).copied().unwrap_or(first);
|
||||
|
||||
@@ -563,7 +563,7 @@ fn backdrop_colors(palette: &Palette) -> [Color; 3] {
|
||||
* one gradient for the pill, the card and the veils; 150 degrees runs from the
|
||||
* top left to the bottom right, like the reference's light
|
||||
*/
|
||||
fn backdrop(colors: &[Color; 3], alpha: u8) -> Gradient {
|
||||
pub fn backdrop(colors: &[Color; 3], alpha: u8) -> Gradient {
|
||||
Gradient::linear(150.0, [(0.0, faded(colors[0], alpha)), (0.55, faded(colors[1], alpha)), (1.0, faded(colors[2], alpha))])
|
||||
}
|
||||
|
||||
@@ -578,7 +578,7 @@ fn deepen(color: Color, max: f32) -> Color {
|
||||
Color::rgb((red * scale) as u8, (green * scale) as u8, (blue * scale) as u8)
|
||||
}
|
||||
|
||||
fn faded(color: Color, alpha: u8) -> Color {
|
||||
pub fn faded(color: Color, alpha: u8) -> Color {
|
||||
Color::rgba(color.red(), color.green(), color.blue(), alpha)
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -3,6 +3,7 @@ mod island;
|
||||
mod parse;
|
||||
mod sound;
|
||||
mod spectrum;
|
||||
mod workspaces;
|
||||
|
||||
use amane::{App, Palette, Service};
|
||||
|
||||
@@ -10,6 +11,11 @@ fn main() {
|
||||
// the card's glow follows the cover art, which art::Art copies to this one path
|
||||
Palette::write().open(&art::palette_file(), 8);
|
||||
|
||||
// one island on every monitor; the card opens only on one of them
|
||||
App::new().font("Fira Code").ipc("island", island::command).window_per_monitor(island::view).run();
|
||||
// one island on every monitor (the card opens only on one of them), and a workspace pill top left on each
|
||||
App::new()
|
||||
.font("Fira Code")
|
||||
.ipc("island", island::command)
|
||||
.window_per_monitor(island::view)
|
||||
.window_per_monitor(workspaces::view)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -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