Tray, fullscreen-only hiding, peeks, Ethernet details, wider maps

- tray.rs: the shell serves org.kde.StatusNotifierWatcher (only when no other
  watcher runs) and is its host; icons from the theme or from IconPixmap
  (png.rs, a stdlib png writer); left/middle/right click and scroll; dbusmenu
  menus drawn in the island's look; tray IPC verbs for testing
- edge.rs: the bar hides only on monitors showing a fullscreen window, driven
  by Hyprland's event socket; peeks of the workspaces (with the disc sliding)
  and the island (song change, new player), with a pop on the island
- prompt.rs: Ethernet details popup (addresses, gateway, DNS, MAC copyable)
- map.rs: 1100x560 maps, every other node a ring further in
- tools/fake_tray.py: a fake tray app, by bus name or by path
This commit is contained in:
server2
2026-10-13 04:58:08 +02:00
parent b14df3a435
commit 1e99d7264c
13 changed files with 1963 additions and 126 deletions
+85 -31
View File
@@ -5,12 +5,12 @@ use std::time::Duration;
use amane::{
Animation, Blend, Center, Color, Full, Horizontal, InputArea, Layer, LayerWindow, Monitor, Padding, Parent, Pointer,
Rectangle, Row, Service, Shadow, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
Rectangle, Row, Service, Shadow, Stack, Start, Text, Vertical, Weight, Widget, Workspace, Workspaces, Zone,
};
use crate::edge::{self, Edge, HIDE_H};
use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, HOVER_MS, SHADOW, TOP_GAP, backdrop, faded};
use crate::theme;
use crate::edge::{self, Edge, HIDE_H, Part};
use crate::island::{CHIP_HOVER, CLEAR, COMPACT_H, HOVER_MS, SHADOW, TOP_GAP, backdrop, faded, mix};
use crate::{theme, tray};
// 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;
@@ -18,6 +18,9 @@ const SLOT_GAP: f32 = 4.0;
const INSET: f32 = 5.0;
const LEFT: f32 = 12.0;
// between the workspaces' pill and the tray's
const TRAY_GAP: f32 = 8.0;
// each hovered workspace's highlight by workspace id, fading between 0 and 1
#[derive(Default)]
struct Hover(HashMap<i64, Animation>);
@@ -43,8 +46,12 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
let workspaces = Workspaces::read();
let hover = Hover::read();
// hidden until the pointer rests on the top edge (edge.rs)
let slide = edge::hidden_by(Edge::read().shown(&monitor.name), 0.0, HIDE_H);
// hidden on a fullscreen monitor until wanted, or for a moment on a workspace change (edge.rs)
let (slide, switched) = {
let edge = Edge::read();
(edge::hidden_by(edge.part(Part::Workspaces, &monitor.name), 0.0, HIDE_H), edge.switched(&monitor.name))
};
// special workspaces (scratchpads) aren't places to switch to
let here: Vec<&Workspace> = workspaces
@@ -57,24 +64,59 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
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");
let window = |window_w: f32| {
LayerWindow::new()
.width(window_w)
.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")
};
// empty and click-through rather than hidden: see the island's view for why
if here.is_empty() || slide <= -HIDE_H {
return window.click_through().child(Rectangle::new().width(0.0).height(0.0));
return window(LEFT + width + SHADOW).click_through().child(Rectangle::new().width(0.0).height(0.0));
}
// the tray's pill right of this one (tray.rs), when an app has an icon there
let tray = tray::pill(&monitor.name, LEFT + width + TRAY_GAP);
let tray_w = tray.as_ref().map_or(0.0, |(_, tray_w)| TRAY_GAP + tray_w);
let window = window(LEFT + width + tray_w + SHADOW);
let mut slots: Vec<Box<dyn Widget>> = Vec::new();
for workspace in here {
/*
* 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
*/
let theme = theme::now();
let slot_x = |index: usize| index as f32 * (SLOT + SLOT_GAP);
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 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), None) => Some(slot_x(to)),
_ => None,
};
let stretch = came_from.map_or(0.0, |(_, t)| (t * std::f32::consts::PI).sin() * 10.0);
if switched.is_some() {
amane::request_frame();
}
for workspace in &here {
let id = workspace.id();
let lit = hover.0.get(&id).map_or(0.0, Animation::value);
@@ -84,10 +126,11 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
_ => workspace.index().to_string(),
};
let theme = theme::now();
let (fill, color, weight) = if workspace.focused() {
(theme.accent, theme.on_accent, Weight::Bold)
// the label turns as the disc arrives
let arrived = switched.map_or(1.0, |(_, t)| t);
(CLEAR, Color::blend(theme.text, theme.on_accent, arrived), Weight::Bold)
} else if workspace.active() {
(faded(theme.accent, 70), theme.accent, Weight::Bold)
} else {
@@ -111,7 +154,16 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
));
}
let theme = theme::now();
let row_w = width - 2.0 * INSET;
// the disc under the focused slot, a little wider while it's on its way
let mut layers: Vec<Box<dyn Widget>> = Vec::new();
if let Some(x) = highlight_x {
layers.push(Box::new(Rectangle::new().width(SLOT + stretch).height(SLOT).radius(Full).fill(theme.accent).translate(x - stretch / 2.0, 0.0)));
}
layers.push(Box::new(Row::new(slots).gap(SLOT_GAP).align(Center)));
let pill = Rectangle::new()
.width(width)
@@ -123,24 +175,26 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
.padding(INSET)
.align_child(Start, Center)
.on_hover(edge::over_bar)
.child(Row::new(slots).gap(SLOT_GAP).align(Center));
.child(Stack::new(layers).width(row_w).height(SLOT));
// 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,
};
// clicks next to the pills reach the windows underneath
let mut regions = vec![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(
let mut pills: Vec<Box<dyn Widget>> = vec![Box::new(pill)];
if let Some((tray, tray_pill_w)) = tray {
regions.push(InputArea { x: (LEFT + width + TRAY_GAP) as i32, y: TOP_GAP as i32, width: tray_pill_w.ceil() as i32, height: COMPACT_H as i32 });
pills.push(Box::new(tray));
}
window.input_region(regions).child(
Rectangle::new()
.width(Parent)
.height(Parent)
.padding(Padding { top: TOP_GAP, right: 0.0, bottom: 0.0, left: LEFT })
.align_child(Start, Start)
.translate(0.0, slide)
.child(pill),
.child(Row::new(pills).gap(TRAY_GAP).align(Start)),
)
}