island: card on one monitor, live bars only on the focused one

- the card opens on the monitor that was clicked (keybinds: the focused
  monitor); the others keep their pill. moving the card to another monitor
  restarts its open animation there
- only the focused monitor's window (and the one with the card) reads
  Spectrum, so the other pills show still bars and rest
- hover lights only the pill under the pointer
- measured with music on 3 monitors: pill 10.7% of a core (was 27.5%),
  open card 20.7% (was 58%), ~62 frames a second in total, 0-1 slow
  frames per open/close (was 7-14), back to one island's cost
This commit is contained in:
server2
2026-10-08 01:25:33 +02:00
parent 967ff37b78
commit 7844a98828
4 changed files with 85 additions and 22 deletions
+76 -19
View File
@@ -5,9 +5,9 @@ use std::time::{Duration, Instant};
use amane::{
Animation, Arc, Argument, Blend, Bus, Canvas, Cap, Center, Color, Column, Easing, End, Full, Gradient,
Image, InputArea, Layer, LayerWindow, Line, Media, MediaPlayer, Padding, Palette, Parent, Path,
Image, InputArea, Layer, LayerWindow, Line, Media, MediaPlayer, Monitor, Padding, Palette, Parent, Path,
Pointer, Rectangle, Row, Scroll, ScrollArea, Service, Shadow, Shape, SpaceBetween, Stack,
Start, Text, Vertical, Weight, Widget, Zone, children, shapes,
Start, Text, Vertical, Weight, Widget, Workspaces, Zone, children, shapes,
};
use crate::art::Art;
@@ -144,18 +144,28 @@ pub struct Island {
// each hovered spot's highlight, fading between 0 and 1
hover: HashMap<Spot, Animation>,
/*
* every monitor has an island, but the card opens on one: the monitor it was last
* opened on (none: every monitor, without a workspace list), and the monitor whose
* pill the pointer was last on, so only that pill lights up
*/
opened_on: Option<String>,
pointer_on: Option<String>,
}
impl Service for Island {
fn new() -> Self {
Self {
open: Animation::new(0.0).duration(Duration::from_millis(OPEN_MS)).easing(Easing::Linear),
open: opening(),
expanded: false,
selected: None,
menu: None,
scrub: None,
seek_shown_until: None,
hover: HashMap::new(),
opened_on: None,
pointer_on: None,
}
}
@@ -220,13 +230,27 @@ struct Scene<'a> {
levels: &'a [f32],
}
pub fn view() -> LayerWindow {
pub fn view(monitor: &Monitor) -> LayerWindow {
let media = Media::read();
let island = Island::read();
let sound = Sound::read();
let art = Art::read();
let palette = Palette::read();
let spectrum = Spectrum::read();
let here = monitor.name.as_str();
let focused = focused_monitor();
// the other monitors keep their pill while the card is open here
let opened_here = island.opened_on.as_deref().is_none_or(|name| name == here);
/*
* only the focused monitor (and the one with the open card) shows live levels:
* reading Spectrum is what redraws a window for every cava frame, so the other
* windows skip it, rest, and show still bars
*/
let live = focused.as_deref().is_none_or(|name| name == here) || (opened_here && island.expanded);
let spectrum = live.then(Spectrum::read);
let levels: &[f32] = spectrum.as_deref().map_or(&[], |spectrum| spectrum.levels.as_slice());
let window_w = WIDE_W + 2.0 * SHADOW;
@@ -243,16 +267,17 @@ pub fn view() -> LayerWindow {
return window.visible(false);
};
let (grow, pill, reveal) = phases(island.open.value());
let (grow, pill, reveal) = phases(if opened_here { island.open.value() } else { 0.0 });
// the pill under the pointer widens a little and glows pink; the card doesn't
let lift = island.lit(Spot::Island) * (1.0 - grow);
let hovered_here = island.pointer_on.as_deref() == Some(here);
let lift = if hovered_here { island.lit(Spot::Island) } else { 0.0 } * (1.0 - grow);
/*
* live levels keep frames coming the way an animation does: a service change
* alone asks with an empty commit, which hyprland only answers on its next repaint
*/
if !spectrum.levels.is_empty() {
if !levels.is_empty() {
amane::request_frame();
}
@@ -271,12 +296,12 @@ pub fn view() -> LayerWindow {
island: &island,
sound: &sound,
art: &art.path,
levels: &spectrum.levels,
levels,
};
veiled(card(&scene), width, height, radius, reveal, &colors)
} else if pill > 0.0 {
veiled(compact(player, &art.path, &spectrum.levels), width, height, radius, pill, &colors)
veiled(compact(player, &art.path, levels), width, height, radius, pill, &colors)
} else {
// mid-grow: the empty shape alone, the cheapest frames of the animation
Rectangle::new().width(0.0).height(0.0)
@@ -297,19 +322,29 @@ pub fn view() -> LayerWindow {
*/
let shape = shape.shadow(Shadow::drop(PINK).opacity(mix(0.3, 0.7, lift)).blur(22.0));
let tapped = monitor.name.clone();
let hovered = monitor.name.clone();
let shape = shape
.align_child(Center, Start)
.on_click(|_| tap())
.on_hover(|inside| {
.on_click(move |_| tap(&tapped))
.on_hover(move |inside| {
if inside {
Island::write().pointer_on = Some(hovered.clone());
}
hovering(Spot::Island)(inside);
if !inside {
// leaving a pill on another monitor leaves the open card alone
let card_here = Island::read().opened_on.as_deref().is_none_or(|name| name == hovered);
if !inside && card_here {
collapse();
}
})
.child(face);
let shape = if island.expanded { shape } else { shape.cursor(Pointer) };
let shape = if island.expanded && opened_here { shape } else { shape.cursor(Pointer) };
// clicks outside the island reach the windows underneath
let region = InputArea {
@@ -329,23 +364,45 @@ pub fn view() -> LayerWindow {
)
}
// a click on the island itself: closes an open dropdown, else opens the card
fn tap() {
// a click on the island of `monitor`: closes an open dropdown, else opens the card there
fn tap(monitor: &str) {
let mut island = Island::write();
if island.menu.take().is_none() {
island.expanded = true;
island.open.to(1.0);
open_on(&mut island, Some(monitor.to_string()));
}
}
// keybinds open the card on the focused monitor
fn expand() {
let mut island = Island::write();
let focused = focused_monitor();
open_on(&mut Island::write(), focused);
}
fn open_on(island: &mut Island, monitor: Option<String>) {
// open on another monitor: that one grows from its pill, this one shows its pill again
if island.opened_on != monitor {
island.open = opening();
island.opened_on = monitor;
}
island.expanded = true;
island.open.to(1.0);
}
// the open timeline, at rest on the pill
fn opening() -> Animation {
Animation::new(0.0).duration(Duration::from_millis(OPEN_MS)).easing(Easing::Linear)
}
// the monitor of the focused workspace, which on hyprland follows the mouse; none without a workspace list
fn focused_monitor() -> Option<String> {
let workspaces = Workspaces::read();
workspaces.list().iter().find(|workspace| workspace.focused()).and_then(|workspace| workspace.output()).map(String::from)
}
fn collapse() {
let mut island = Island::write();