Esc closes the config window; a removed empty workspace closes up during the switch

Hyprland drops an empty workspace the moment it's left, so its number vanished
and the row jumped before the disc slid. Its slot now stays for the slide,
shrinking and fading, and the pill narrows with it.
This commit is contained in:
server2
2026-10-15 02:55:30 +02:00
parent 4fbfad3ce3
commit 3b9929aff4
2 changed files with 54 additions and 15 deletions
+7 -1
View File
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::process::Command;
use amane::{
Center, Color, Column, Cursor, End, Full, Padding, Rectangle, Row, ScrollArea, Service, SpaceBetween, Stack, Start, Text, TextInput, Weight, Widget, Window, children, close_window, open_window, window_size,
Center, Color, Column, Cursor, End, Full, Key, Padding, Rectangle, Row, ScrollArea, Service, SpaceBetween, Stack, Start, Text, TextInput, Weight, Widget, Window, children, close_window, open_window, window_size,
};
use crate::config::{self, Kind, Opt};
@@ -283,6 +283,12 @@ pub fn view() -> Window {
.title(TITLE)
.size(1100.0, 760.0)
.resizable(true)
// Escape closes it, also from a field (a typed value only applies on Enter)
.on_key(|key| {
if key == Key::Escape {
close_window(NAME);
}
})
.child(
Rectangle::new()
.width(width)
+47 -14
View File
@@ -60,7 +60,27 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
.collect();
let count = here.len() as f32;
let width = 2.0 * pill::inset() + count * pill::slot() + (count - 1.0).max(0.0) * pill::slot_gap();
let step = pill::slot() + pill::slot_gap();
// ease in and out, with a slight stretch in the middle like a drop of ink
let eased = |t: f32| t * t * (3.0 - 2.0 * t);
/*
* hyprland removes an empty workspace the moment it's left. its slot stays for the
* slide, fading and closing up, so the row and the pill narrow with the disc's move
* instead of jumping first: (where it was, its label, how much of it is left, 1 to 0)
*/
let gone = switched.filter(|(from, _)| *from > 0 && here.iter().all(|workspace| workspace.id() != *from)).map(|(from, t)| {
let at = here.iter().filter(|workspace| i64::from(workspace.index()) < from).count();
(at, from.to_string(), 1.0 - eased(t))
});
let left = gone.as_ref().map_or(0.0, |(_, _, left)| *left);
let width = 2.0 * pill::inset() + count * pill::slot() + (count - 1.0).max(0.0) * pill::slot_gap() + left * step;
// the window keeps the ghost's full room while it closes, so it isn't resized every frame
let room = if gone.is_some() { width + (1.0 - left) * step } else { width };
let window = |window_w: f32| {
LayerWindow::new()
@@ -83,27 +103,26 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
let tray = if peeking { None } else { tray::pill(&monitor.name, pill::left() + width + pill::tray_gap()) };
let tray_w = tray.as_ref().map_or(0.0, |(_, tray_w)| pill::tray_gap() + tray_w);
let window = window(pill::left() + width + tray_w + island_pill::shadow());
let window = window(pill::left() + room + tray_w + island_pill::shadow());
let mut slots: Vec<Box<dyn Widget>> = Vec::new();
/*
* the focused workspace's accent is one disc under the row, so a change slides it from
* the workspace before to this one (edge.rs times it); the slots themselves stay clear
* the workspace before to this one (edge.rs times it); the slots themselves stay clear.
* slots are placed by x in one stack, after a closing ghost by what's left of it
*/
let theme = theme::now();
let slot_x = |index: usize| index as f32 * (pill::slot() + pill::slot_gap());
let slot_x = |index: usize| index as f32 * step + gone.as_ref().filter(|(at, _, _)| index >= *at).map_or(0.0, |_| left * step);
let focused_at = here.iter().position(|workspace| workspace.focused());
let came_from = switched.and_then(|(from, t)| here.iter().position(|workspace| workspace.id() == from).map(|at| (at, t)));
let came_from = switched.and_then(|(from, t)| match &gone {
Some((at, _, _)) => Some((*at as f32 * step, t)),
None => here.iter().position(|workspace| workspace.id() == from).map(|at| (slot_x(at), t)),
});
let highlight_x = match (focused_at, came_from) {
(Some(to), Some((from, t))) => {
// ease in and out, with a slight stretch in the middle like a drop of ink
let eased = t * t * (3.0 - 2.0 * t);
Some(mix(slot_x(from), slot_x(to), eased))
}
(Some(to), Some((from_x, t))) => Some(mix(from_x, slot_x(to), eased(t))),
(Some(to), None) => Some(slot_x(to)),
_ => None,
};
@@ -114,7 +133,20 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
amane::request_frame();
}
for workspace in &here {
// the ghost: its number shrinking into the gap it leaves
if let Some((at, label, left)) = &gone {
slots.push(Box::new(
Rectangle::new()
.width(pill::slot())
.height(pill::slot())
.align_child(Center, Center)
.scale(*left)
.translate(*at as f32 * step - (1.0 - left) * pill::slot() / 2.0, 0.0)
.child(Text::new(label.as_str()).size(text::body()).color(Color::blend(CLEAR, theme.dim, *left))),
));
}
for (at, workspace) in here.iter().enumerate() {
let id = workspace.id();
let lit = hover.0.get(&id).map_or(0.0, Animation::value);
@@ -148,20 +180,21 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
.cursor(Pointer)
.on_hover(move |inside| hovering(id, inside))
.on_click(move |_| focus(id))
.translate(slot_x(at), 0.0)
.child(Text::new(label).size(text::body()).weight(weight).color(color)),
));
}
let row_w = width - 2.0 * pill::inset();
// the disc under the focused slot, a little wider while it's on its way
// the disc under the focused slot, a little wider while it's on its way, then the slots over it
let mut layers: Vec<Box<dyn Widget>> = Vec::new();
if let Some(x) = highlight_x {
layers.push(Box::new(Rectangle::new().width(pill::slot() + stretch).height(pill::slot()).radius(Full).fill(theme.accent).translate(x - stretch / 2.0, 0.0)));
}
layers.push(Box::new(Row::new(slots).gap(pill::slot_gap()).align(Center)));
layers.extend(slots);
let pill = Rectangle::new()
.width(width)