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:
@@ -30,7 +30,9 @@ The equalizer: one `setsid -f pipewire -c eq-<key>.conf` process per player with
|
|||||||
|
|
||||||
### The view (island.rs `view`)
|
### The view (island.rs `view`)
|
||||||
|
|
||||||
- One `LayerWindow` (namespace `magic-island`, `Layer::Overlay` because the user's windows are fullscreen) sized for the open card; an `InputArea` lets clicks outside the island through.
|
- One `LayerWindow` per monitor (`window_per_monitor`, namespace `magic-island`, `Layer::Overlay` because the user's windows are fullscreen) sized for the open card; an `InputArea` lets clicks outside the island through.
|
||||||
|
- All windows share the `Island` state. `opened_on` names the monitor the card is open on (others show their pill), `pointer_on` the monitor whose pill is hovered. Only the focused monitor's window (and the one with the card) reads `Spectrum`, so the others don't redraw for every cava frame: with music, pill ~11% of a core and open card ~21%, as with one island. `focused_monitor()` comes from `Workspaces`, which follows the mouse on Hyprland.
|
||||||
|
- `Spectrum::listen` first `pkill -KILL -f`s any cava running our `cava.conf`: a stopped shell leaves its cava behind, and cava ignores SIGTERM.
|
||||||
- `phases(open)` splits the one linear 560 ms timeline: pill fades out over 0–0.18, the empty shape grows over 0–0.6 (cubic ease-out), the card fades in over 0.6–1 (smoothstep). Closing runs it backwards. At most one face (pill or card) is built per frame.
|
- `phases(open)` splits the one linear 560 ms timeline: pill fades out over 0–0.18, the empty shape grows over 0–0.6 (cubic ease-out), the card fades in over 0.6–1 (smoothstep). Closing runs it backwards. At most one face (pill or card) is built per frame.
|
||||||
- `veiled()` fades a face by laying the backdrop gradient over it with alpha `1 - shown` instead of `.opacity()`: same picture, no GPU layer.
|
- `veiled()` fades a face by laying the backdrop gradient over it with alpha `1 - shown` instead of `.opacity()`: same picture, no GPU layer.
|
||||||
- `backdrop_colors()` / `backdrop()`: one 150° three-stop gradient from the palette (dominant, `colors()[1]`, `background()`), each darkened by `deepen()` to a max brightness of 0.22 / 0.15 / 0.07.
|
- `backdrop_colors()` / `backdrop()`: one 150° three-stop gradient from the palette (dominant, `colors()[1]`, `background()`), each darkened by `deepen()` to a max brightness of 0.22 / 0.15 / 0.07.
|
||||||
@@ -96,6 +98,9 @@ tools/amane-remote.sh $H sh 'COMMAND' # anything inside the graphical sessi
|
|||||||
|
|
||||||
- Pick a fake player with `ipc island next` until `status` names it, before anything that changes sound.
|
- Pick a fake player with `ipc island next` until `status` names it, before anything that changes sound.
|
||||||
- The island follows the focused monitor: read its geometry from `hyprctl layers -j` (namespace `magic-island`) right before each `grim` capture.
|
- The island follows the focused monitor: read its geometry from `hyprctl layers -j` (namespace `magic-island`) right before each `grim` capture.
|
||||||
|
- With one window per monitor, `fps` and `anim` count every window's frames together; resting windows add ~1000 ms gaps (one frame a second for the time bar). Those aren't stalls.
|
||||||
|
- The desktop's `$XDG_RUNTIME_DIR/hypr/` keeps folders of dead Hyprland instances; the live one is the one with `.socket.sock`, which `amane-remote.sh` now picks. A shell started with a dead signature gets an empty `Workspaces` list.
|
||||||
|
- Once, a fresh shell exited with "lost the gpu (Device is lost)" (Mesa warns the Xe driver is experimental); starting it again worked.
|
||||||
- The user's pointer may be over the island and open or collapse it during a test; check `status` and capture right after `open`.
|
- The user's pointer may be over the island and open or collapse it during a test; check `status` and capture right after `open`.
|
||||||
- To tune a visualiser, sample the real levels read-only (`pw-record --target <stream> | cava`, with `timeout -s KILL` on each stage; a plain `timeout` on a pipeline orphans cava) and look at the distribution first. Typical music sits at 0.15–0.35.
|
- To tune a visualiser, sample the real levels read-only (`pw-record --target <stream> | cava`, with `timeout -s KILL` on each stage; a plain `timeout` on a pipeline orphans cava) and look at the distribution first. Typical music sits at 0.15–0.35.
|
||||||
- In `pkill -f` patterns over SSH, use `[p]ython3 ...` so the pattern doesn't match the remote shell itself.
|
- In `pkill -f` patterns over SSH, use `[p]ython3 ...` so the pattern doesn't match the remote shell itself.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ A Dynamic-Island-style media widget for Amane (Rust Wayland shell library), runn
|
|||||||
- **Real 10-band equalizer per player** (31 Hz to 16 kHz, ±12 dB): a PipeWire filter-chain per player, with 8 presets (Flat, Bass, Treble, Vocal, Pop, Rock, Jazz, Classic). Curves are saved per player.
|
- **Real 10-band equalizer per player** (31 Hz to 16 kHz, ±12 dB): a PipeWire filter-chain per player, with 8 presets (Flat, Bass, Treble, Vocal, Pop, Rock, Jazz, Classic). Curves are saved per player.
|
||||||
- **Background:** a diagonal gradient from the cover's own colours, darkened so white text stays readable.
|
- **Background:** a diagonal gradient from the cover's own colours, darkened so white text stays readable.
|
||||||
- **Opening animation:** the pill fades out, the empty shape grows, then the card fades in. Closing runs backwards. Moving the pointer off the open card closes it.
|
- **Opening animation:** the pill fades out, the empty shape grows, then the card fades in. Closing runs backwards. Moving the pointer off the open card closes it.
|
||||||
|
- **Every monitor:** each monitor shows the pill. The card opens only on the monitor you click (a keybind opens it on the focused one), and only the focused monitor's pill shows live bars; the others show still bars, which keeps the cost at about one island's.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
|||||||
+76
-19
@@ -5,9 +5,9 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use amane::{
|
use amane::{
|
||||||
Animation, Arc, Argument, Blend, Bus, Canvas, Cap, Center, Color, Column, Easing, End, Full, Gradient,
|
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,
|
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;
|
use crate::art::Art;
|
||||||
@@ -144,18 +144,28 @@ pub struct Island {
|
|||||||
|
|
||||||
// each hovered spot's highlight, fading between 0 and 1
|
// each hovered spot's highlight, fading between 0 and 1
|
||||||
hover: HashMap<Spot, Animation>,
|
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 {
|
impl Service for Island {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
open: Animation::new(0.0).duration(Duration::from_millis(OPEN_MS)).easing(Easing::Linear),
|
open: opening(),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
selected: None,
|
selected: None,
|
||||||
menu: None,
|
menu: None,
|
||||||
scrub: None,
|
scrub: None,
|
||||||
seek_shown_until: None,
|
seek_shown_until: None,
|
||||||
hover: HashMap::new(),
|
hover: HashMap::new(),
|
||||||
|
opened_on: None,
|
||||||
|
pointer_on: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,13 +230,27 @@ struct Scene<'a> {
|
|||||||
levels: &'a [f32],
|
levels: &'a [f32],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view() -> LayerWindow {
|
pub fn view(monitor: &Monitor) -> LayerWindow {
|
||||||
let media = Media::read();
|
let media = Media::read();
|
||||||
let island = Island::read();
|
let island = Island::read();
|
||||||
let sound = Sound::read();
|
let sound = Sound::read();
|
||||||
let art = Art::read();
|
let art = Art::read();
|
||||||
let palette = Palette::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;
|
let window_w = WIDE_W + 2.0 * SHADOW;
|
||||||
|
|
||||||
@@ -243,16 +267,17 @@ pub fn view() -> LayerWindow {
|
|||||||
return window.visible(false);
|
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
|
// 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
|
* 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
|
* 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();
|
amane::request_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,12 +296,12 @@ pub fn view() -> LayerWindow {
|
|||||||
island: &island,
|
island: &island,
|
||||||
sound: &sound,
|
sound: &sound,
|
||||||
art: &art.path,
|
art: &art.path,
|
||||||
levels: &spectrum.levels,
|
levels,
|
||||||
};
|
};
|
||||||
|
|
||||||
veiled(card(&scene), width, height, radius, reveal, &colors)
|
veiled(card(&scene), width, height, radius, reveal, &colors)
|
||||||
} else if pill > 0.0 {
|
} 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 {
|
} else {
|
||||||
// mid-grow: the empty shape alone, the cheapest frames of the animation
|
// mid-grow: the empty shape alone, the cheapest frames of the animation
|
||||||
Rectangle::new().width(0.0).height(0.0)
|
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 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
|
let shape = shape
|
||||||
.align_child(Center, Start)
|
.align_child(Center, Start)
|
||||||
.on_click(|_| tap())
|
.on_click(move |_| tap(&tapped))
|
||||||
.on_hover(|inside| {
|
.on_hover(move |inside| {
|
||||||
|
if inside {
|
||||||
|
Island::write().pointer_on = Some(hovered.clone());
|
||||||
|
}
|
||||||
|
|
||||||
hovering(Spot::Island)(inside);
|
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();
|
collapse();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(face);
|
.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
|
// clicks outside the island reach the windows underneath
|
||||||
let region = InputArea {
|
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
|
// a click on the island of `monitor`: closes an open dropdown, else opens the card there
|
||||||
fn tap() {
|
fn tap(monitor: &str) {
|
||||||
let mut island = Island::write();
|
let mut island = Island::write();
|
||||||
|
|
||||||
if island.menu.take().is_none() {
|
if island.menu.take().is_none() {
|
||||||
island.expanded = true;
|
open_on(&mut island, Some(monitor.to_string()));
|
||||||
island.open.to(1.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keybinds open the card on the focused monitor
|
||||||
fn expand() {
|
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.expanded = true;
|
||||||
island.open.to(1.0);
|
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() {
|
fn collapse() {
|
||||||
let mut island = Island::write();
|
let mut island = Island::write();
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -10,6 +10,6 @@ fn main() {
|
|||||||
// the card's glow follows the cover art, which art::Art copies to this one path
|
// the card's glow follows the cover art, which art::Art copies to this one path
|
||||||
Palette::write().open(&art::palette_file(), 8);
|
Palette::write().open(&art::palette_file(), 8);
|
||||||
|
|
||||||
// one island on every monitor, all showing the same player and state
|
// 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();
|
App::new().font("Fira Code").ipc("island", island::command).window_per_monitor(island::view).run();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user