diff --git a/src/settings.rs b/src/settings.rs index 66480ef..01adbe7 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -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) diff --git a/src/workspaces.rs b/src/workspaces.rs index 76b2ce8..73f1e49 100644 --- a/src/workspaces.rs +++ b/src/workspaces.rs @@ -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> = 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> = 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)