Ethernet details in the panel, smaller maps, no tray in workspace peeks

- control.rs: Menu::Ethernet; the panel morphs into the card's details
  (gathered off the drawing thread first) instead of a separate popup;
  prompt::copy now takes a callback so both use wl-copy the same way
- prompt.rs: the Details popup is gone
- map.rs: 920x460, at most 8 nodes, lighter nearness pull (simulated:
  no label overlaps up to 8 nodes while drifting)
- workspaces.rs: a peek on a workspace change shows the workspaces alone
This commit is contained in:
server2
2026-10-13 05:40:44 +02:00
parent 1e99d7264c
commit a7c49a16c0
5 changed files with 139 additions and 132 deletions
+3 -3
View File
@@ -15,7 +15,7 @@ A Dynamic-Island-style media widget for Amane (Rust Wayland shell library), runn
- **System tray:** right of the workspace pill, a pill with every tray app's icon (StatusNotifierItem). Left click activates the app (or opens its menu if that's all it offers), right click opens its menu, drawn in the island's look (checkmarks, separators, submenus with a way back, disabled entries dim), middle click is the app's secondary action, scrolling its scroll. Nothing on this desktop ran a tray watcher, so the shell owns `org.kde.StatusNotifierWatcher` on the session bus and serves it (if another bar already does, the shell shows no tray). Icons come from the app's icon name (its own theme folder, gtk's icon theme, hicolor, AdwaitaLegacy, Adwaita, `/usr/share/pixmaps`) or from the pixels it sends, written as PNGs to `$XDG_RUNTIME_DIR/amane-tray/`. An app that quits takes its icon with it (`src/tray.rs`).
- **Control centre:** top right on every monitor, a pill with Ethernet, Wi-Fi (or the hotspot), VPN, Bluetooth and the battery. Click it and it opens, in the island's way, into a panel like macOS's Control Center. See [Control centre](#control-centre).
- **Out of the way of fullscreen windows:** on a monitor whose workspace shows a fullscreen window (Hyprland fullscreen 2 or 3; only maximised keeps the bar), the island, the workspace pill (with the tray) and the control centre's pill slide out of sight. Resting the pointer on that monitor's top edge for 0.3 s brings them back until it has been off them and the edge for 0.7 s; an open card or panel stays. Everywhere else the bar is always there. Hyprland's event socket says when windows go fullscreen or workspaces change, and its command socket answers `monitors` and `clients`; nothing polls. The edge strip is a one-pixel Overlay layer, there only on fullscreen monitors (it takes their top pixel row). While hidden the bar draws nothing at all, a playing player included (`src/edge.rs`).
- **Peeks:** some events bring one part in for a moment over a fullscreen window: the workspace pill for 1.6 s when the workspace changes (with the disc sliding over), the island for 3.5 s when the song changes or a player appears, on the focused monitor. The island also pops (it widens and glows, then lets go) on those events when it's visible anyway. The control centre never peeks.
- **Peeks:** some events bring one part in for a moment over a fullscreen window: the workspace pill alone (without the tray) for 1.6 s when the workspace changes (with the disc sliding over), the island for 3.5 s when the song changes or a player appears, on the focused monitor. The island also pops (it widens and glows, then lets go) on those events when it's visible anyway. The control centre never peeks.
## Control centre
@@ -28,9 +28,9 @@ The panel has:
- *Bluetooth* (BlueZ, through Amane's `Bluetooth`): the switch powers the adapter; with bluez installed but stopped it runs `rc-service bluetooth start`. The menu is the same map with the devices bluez knows or finds (it scans while open): connected ones closest, then paired, then the rest. Click to connect, disconnect, or pair (then trust and connect). Pairing and connecting go through a `bluetoothctl` session with its own agent: a PIN or passkey it needs, a code to confirm or to type on the device are asked in a popup, and a failure shows why. Click the bubble to search for devices (`bluetoothctl --timeout 15 scan on`).
- *VPN*: every openrc service `/etc/init.d/openvpn.*` (here `openvpn.swiss`), each with a switch. Connecting while openrc has it *inactive*, connected once its `up.sh` marks it *started*. A switch runs `rc-service openvpn.<name> start|stop`.
- *Hotspot* (iwd access point): see [Hotspot setup](#hotspot-setup).
- *Ethernet*: every real wired card in `/sys/class/net` (a device behind it, not wireless): connected or no cable, and the link speed it negotiated (`speed`, as 1 Gb/s). A click opens a popup beside the panel with the interface and driver, status (speed, duplex), every IPv4 and IPv6 address, gateway, DNS servers, MAC, MTU and traffic since boot (`ip -o addr`, `ip -o route`, `/etc/resolv.conf`, sysfs). Addresses, gateway, DNS and MAC have a Copy chip; a click anywhere on such a row copies it (addresses without their prefix).
- *Ethernet*: every real wired card in `/sys/class/net` (a device behind it, not wireless): connected or no cable, and the link speed it negotiated (`speed`, as 1 Gb/s). A click turns the panel into its details (it morphs like into any menu; the back button returns): the interface and driver, status (speed, duplex), every IPv4 and IPv6 address, gateway, DNS servers, MAC, MTU and traffic since boot (`ip -o addr`, `ip -o route`, `/etc/resolv.conf`, sysfs). Addresses, gateway, DNS and MAC have a Copy chip; a click anywhere on such a row copies it (addresses without their prefix).
- **Popups:** questions (a password, PIN or passkey), a code to confirm, a saved password or why something failed, in a small card over the menu. Only a popup with a field takes the keyboard (Enter answers, Escape cancels); the others leave keys to other windows until clicked.
- **Menus:** one shape the whole way: the panel fades its content out and takes the menu's size (the Wi-Fi and Bluetooth maps are 1100 × 560 px, three times the panel's width, with every other node a ring further in so neighbouring labels don't meet), then the menu's content fades in over it from a window of its own (so the maps' animation redraws only that window). The back button returns to the panel. Moving the pointer off both closes the menu where it is: its content fades out and it shrinks straight into the pill, without the panel growing back first.
- **Menus:** one shape the whole way: the panel fades its content out and takes the menu's size (the Wi-Fi and Bluetooth maps are 920 × 460 px, with at most 8 networks or devices, the strongest first, every other one a little further in so neighbouring labels don't meet), then the menu's content fades in over it from a window of its own (so the maps' animation redraws only that window). The back button returns to the panel. Moving the pointer off both closes the menu where it is: its content fades out and it shrinks straight into the pill, without the panel growing back first.
- **Sound:** the default output's volume (drag or scroll the slider); the value next to the title is the mute button. With `extra_output` in `shell.conf` a second slider follows for the output whose name contains it (here `NR515`: the `[TX-NR515]` receiver on HDMI 1). Clicking the block elsewhere opens the Sound menu: every output with its own slider and mute; click a name to make it the default output.
- **Display:** the laptop screen's brightness and the keyboard light (`tpacpi::kbd_backlight`: Off, Low, High), written straight to their sysfs files (a udev rule makes them writable by everyone on this laptop), else through logind, which refuses a shell without a seat (one started over ssh). For external monitors see [External monitor brightness](#external-monitor-brightness).
- **Performance:** the firmware profiles in `/sys/firmware/acpi/platform_profile_choices` (Low power, Balanced, Performance), and the battery's state and time left. Picking one sets the platform profile and the CPU governor and energy preference with it: Low power = `powersave` + `power`, Balanced = `powersave` + `balance_performance`, Performance = the `performance` governor.
+102 -17
View File
@@ -33,6 +33,12 @@ const MENU_PAD: f32 = 12.0;
const MENU_W: f32 = PANEL_W - 2.0 * MENU_PAD;
const MENU_HEADER_H: f32 = 28.0;
const MENU_ROW_H: f32 = 44.0;
// the Ethernet menu's detail rows
const ETHERNET_W: f32 = 500.0;
const DETAIL_H: f32 = 28.0;
const DETAIL_LABEL_W: f32 = 78.0;
const COPY_W: f32 = 58.0;
const MENU_ROWS: usize = 7;
const NOTE_H: f32 = 18.0;
const OUTPUT_H: f32 = 22.0;
@@ -49,7 +55,7 @@ const PILL_PAD: f32 = 16.0;
const PILL_GAP: f32 = 12.0;
const PILL_HOVER_GROW: f32 = 6.0;
pub const PANEL_W: f32 = 360.0;
const PANEL_W: f32 = 360.0;
const PANEL_RADIUS: f32 = 26.0;
const PAD: f32 = 12.0;
const INNER_W: f32 = PANEL_W - 2.0 * PAD;
@@ -106,6 +112,7 @@ pub enum Menu {
Bluetooth,
Vpn,
Sound,
Ethernet,
}
// everything that lights up under the pointer
@@ -156,6 +163,10 @@ pub struct Control {
// when the bubble in a map was clicked to search again: rings run out from it
scan_at: Option<Instant>,
// the wired card the Ethernet menu shows, what it knew when it opened (label, value, what a click copies), and the row just copied
ethernet: (String, Vec<(String, String, Option<String>)>),
copied: Option<usize>,
}
impl Service for Control {
@@ -175,6 +186,8 @@ impl Service for Control {
inside_menu: false,
pending: None,
scan_at: None,
ethernet: (String::new(), Vec::new()),
copied: None,
}
}
@@ -638,6 +651,35 @@ fn open_menu(menu: Menu) {
}
}
/*
* a click on the Ethernet tile: the card's details are gathered off the drawing thread
* (ip, sysfs), then the panel turns into them like into any menu
*/
fn open_ethernet() {
thread::spawn(|| {
let card = {
let links = Links::read();
links.wired.iter().find(|card| card.connected).or(links.wired.first()).map(|card| card.name.clone())
};
let Some(card) = card else {
return;
};
let details = system::wired_details(&card);
{
let mut control = Control::write();
control.ethernet = (card, details);
control.copied = None;
}
open_menu(Menu::Ethernet);
});
}
fn close_menu() {
let menu = {
let mut control = Control::write();
@@ -702,18 +744,7 @@ pub fn command(arguments: &[String]) -> String {
["wifi", "search"] => search_again(Menu::Wifi),
["bluetooth", "search"] => search_again(Menu::Bluetooth),
// what a click on the Ethernet tile does
["ethernet"] => {
let card = {
let links = Links::read();
links.wired.iter().find(|card| card.connected).or(links.wired.first()).map(|card| card.name.clone())
};
match card {
Some(card) => prompt::wired(&card),
None => return String::from("no wired card"),
}
}
["ethernet"] => open_ethernet(),
["bluetooth", action @ ("pair" | "connect"), address] => {
let name = Bluetooth::read().devices().iter().find(|device| device.address() == *address).map_or(address.to_string(), |device| device.name().to_string());
@@ -735,6 +766,7 @@ fn status() -> String {
Some(Menu::Bluetooth) => "bluetooth",
Some(Menu::Vpn) => "vpn",
Some(Menu::Sound) => "sound",
Some(Menu::Ethernet) => "ethernet",
None => "none",
};
@@ -953,10 +985,8 @@ fn tiles(scene: &Scene) -> Column {
];
if scene.layout.ethernet {
// a click lists what there is to know about the card, in a popup beside the panel
let card = scene.links.wired.iter().find(|card| card.connected).or(scene.links.wired.first()).map(|card| card.name.clone()).unwrap_or_default();
rows.push(Box::new(tile(control, Tile::Ethernet, "Ethernet", ethernet_state(scene.links), None, INNER_W).cursor(Pointer).on_click(move |_| prompt::wired(&card))));
// a click turns the panel into what there is to know about the card
rows.push(Box::new(tile(control, Tile::Ethernet, "Ethernet", ethernet_state(scene.links), None, INNER_W).cursor(Pointer).on_click(|_| open_ethernet())));
}
Column::new(rows).gap(GAP)
@@ -1653,6 +1683,7 @@ fn menu_content(menu: Menu, control: &Control, links: &Links, bluetooth: &Blueto
Menu::Bluetooth => bluetooth_menu(control, bluetooth, links),
Menu::Vpn => vpn_menu(control, links),
Menu::Sound => sound_menu(control, sound),
Menu::Ethernet => ethernet_menu(control),
}
}
@@ -2064,6 +2095,60 @@ fn vpn_menu(control: &Control, links: &Links) -> MenuContent {
body.into_content("VPN", None, PANEL_W)
}
/*
* the wired card as the panel shows it once clicked: label, value, and on a row with
* something to copy a Copy chip (the whole row copies). wider than the panel, so an
* IPv6 address with its prefix fits
*/
fn ethernet_menu(control: &Control) -> MenuContent {
let theme = theme::now();
let (card, details) = &control.ethernet;
let row_w = ETHERNET_W - 2.0 * MENU_PAD;
let value_w = row_w - DETAIL_LABEL_W - COPY_W - 22.0;
let mut body = Body::new(0.0);
for (index, (label, value, copy_text)) in details.iter().enumerate() {
let lit = if copy_text.is_some() { control.lit(Spot::Row(index)) } else { 0.0 };
let copied = control.copied == Some(index);
let chip: Box<dyn Widget> = if copy_text.is_some() {
let ink = if copied { theme.accent } else { Color::blend(theme.dim, theme.text, lit) };
Box::new(Rectangle::new().width(COPY_W).height(20.0).radius(6.0).fill(Color::blend(CHIP, CHIP_HOVER, lit)).align_child(Center, Center).child(Text::new(if copied { "Copied" } else { "Copy" }).size(10.0).color(ink)))
} else {
Box::new(Rectangle::new().width(COPY_W).height(20.0))
};
let line = Rectangle::new()
.width(row_w)
.height(DETAIL_H)
.radius(8.0)
.fill(Color::blend(CLEAR, CHIP_HOVER, 0.6 * lit))
.padding(Padding { top: 0.0, right: 4.0, bottom: 0.0, left: 8.0 })
.align_child(Start, Center)
.child(
Row::new(vec![
Box::new(Rectangle::new().width(DETAIL_LABEL_W).height(DETAIL_H).align_child(Start, Center).child(Text::new(label.as_str()).size(10.5).color(theme.dim))) as Box<dyn Widget>,
Box::new(Rectangle::new().width(value_w).height(DETAIL_H).align_child(Start, Center).child(Text::new(value.as_str()).size(11.5).color(theme.text).elide())),
chip,
])
.gap(6.0)
.align(Center),
);
match copy_text.clone() {
Some(text) => body.add(line.cursor(Pointer).on_hover(hovering(Spot::Row(index))).on_click(move |_| prompt::copy(&text, move || Control::write().copied = Some(index))), DETAIL_H),
None => body.add(line, DETAIL_H),
}
}
let right: Box<dyn Widget> = Box::new(Text::new(card.as_str()).size(11.0).color(theme.dim));
body.into_content("Ethernet", Some(right), ETHERNET_W)
}
// every output with its own volume; a click on its name makes it the default
fn sound_menu(control: &Control, sound: &Sound) -> MenuContent {
let theme = theme::now();
+6 -6
View File
@@ -16,10 +16,10 @@ use crate::island::{CHIP_OPEN, faded};
use crate::theme;
// how wide the map menus are, against the panel's 360
pub const MAP_W: f32 = 1100.0;
pub const MAP_W: f32 = 920.0;
// the map's own height, under the menu's header
pub const AREA_H: f32 = 560.0;
pub const AREA_H: f32 = 460.0;
const BUBBLE: f32 = 40.0;
const NODE_R: f32 = 22.0;
@@ -28,14 +28,14 @@ const LABEL_W: f32 = 140.0;
const LABEL_H: f32 = 30.0;
// every other one sits this much further in
const STAGGER: f32 = 0.78;
const STAGGER: f32 = 0.9;
// how far each one floats from its place, either way
const DRIFT_X: f32 = 26.0;
const DRIFT_Y: f32 = 16.0;
// at most this many around the bubble
pub const NODES: usize = 12;
// at most this many around the bubble (the strongest come first): more don't fit at this size without labels meeting
pub const NODES: usize = 8;
// the wobbling line of what isn't connected
const IDLE_LINE: Color = Color::rgba(205, 205, 212, 85);
@@ -118,7 +118,7 @@ pub fn map(width: f32, height: f32, centre: Centre, nodes: Vec<Node>) -> Rectang
// evenly round, a quarter step past the top, so no one sits right on top of or under the bubble
let angle = -PI / 2.0 + TAU * (phase + 0.25) / count;
// the stronger the nearer, but never far in; every other one a step further in, so neighbours' labels don't meet
let reach = (1.0 - 0.25 * node.nearness.clamp(0.0, 1.0)) * if index % 2 == 1 { STAGGER } else { 1.0 };
let reach = (1.0 - 0.1 * node.nearness.clamp(0.0, 1.0)) * if index % 2 == 1 { STAGGER } else { 1.0 };
// pushed out to the inner ellipse if the nearness took it inside
let (ex, ey) = (angle.cos() * rx * reach, angle.sin() * ry * reach);
+24 -102
View File
@@ -18,18 +18,15 @@ use amane::{
Zone, children,
};
use crate::control::{self, Control, PANEL_W, RIGHT};
use crate::control::{self, Control, RIGHT};
use crate::glyph::{Glyph, icon};
use crate::island::{self, CHIP, CHIP_HOVER, CHIP_OPEN, CLEAR, HOVER_MS, SHADOW, TOP_GAP, backdrop, veiled};
use crate::island::{self, CHIP, CHIP_HOVER, CHIP_OPEN, HOVER_MS, SHADOW, TOP_GAP, backdrop, veiled};
use crate::map::MAP_W;
use crate::parse::{self, Pairing};
use crate::system;
use crate::theme;
const POPUP_W: f32 = 360.0;
// the Ethernet details: wide enough for an IPv6 address with its prefix
const DETAILS_W: f32 = 500.0;
const POPUP_PAD: f32 = 16.0;
const INNER_W: f32 = POPUP_W - 2.0 * POPUP_PAD;
const HEADER_H: f32 = 36.0;
@@ -39,8 +36,6 @@ const LINE_H: f32 = 18.0;
const BUTTON_W: f32 = 92.0;
const BUTTON_H: f32 = 32.0;
const GAP: f32 = 14.0;
const DETAIL_H: f32 = 26.0;
const LABEL_W: f32 = 78.0;
// a bluetoothctl session that hasn't ended by then is stopped
const SESSION_S: u64 = 90;
@@ -63,9 +58,6 @@ pub enum Ask {
// why joining, pairing or connecting didn't work
Failed { title: String, reason: String },
// what there is to know about a wired card: (label, value, what Copy takes)
Details(Vec<(String, String, Option<String>)>),
}
#[derive(Clone, PartialEq)]
@@ -186,19 +178,8 @@ pub fn reveal(ssid: &str) {
});
}
// a click on the Ethernet tile: what there is to know about the card, gathered off the drawing thread
pub fn wired(card: &str) {
let card = card.to_string();
thread::spawn(move || {
let rows = system::wired_details(&card);
open(Ask::Details(rows), false, &card, "");
});
}
// a password or an address to the clipboard through wl-copy, on its input so it never shows in the process list
fn copy(password: &str, row: usize) {
// a password or an address to the clipboard through wl-copy, on its input so it never shows in the process list; `done` once it's there
pub fn copy(password: &str, done: impl FnOnce() + Send + 'static) {
let password = password.to_string();
thread::spawn(move || {
@@ -210,7 +191,7 @@ fn copy(password: &str, row: usize) {
// wl-copy leaves a child of its own behind to serve the clipboard and exits
if child.wait().is_ok_and(|status| status.success()) && written {
Prompt::write().copied = Some(row);
done();
}
});
}
@@ -391,7 +372,7 @@ fn ready(ask: &Ask, typed: &str) -> bool {
Ask::Pin => !typed.trim().is_empty(),
Ask::Passkey => typed.trim().parse::<u32>().is_ok_and(|passkey| passkey <= 999_999),
Ask::Show(_) => false,
Ask::Confirm(_) | Ask::Saved(_) | Ask::Failed { .. } | Ask::Details(_) => true,
Ask::Confirm(_) | Ask::Saved(_) | Ask::Failed { .. } => true,
}
}
@@ -411,7 +392,7 @@ pub fn answer(typed: &str) {
Ask::Password => joining(request.target, Some(typed.to_string())),
Ask::Pin | Ask::Passkey => send(&format!("{}\n", typed.trim())),
Ask::Confirm(_) => send("yes\n"),
Ask::Show(_) | Ask::Saved(_) | Ask::Failed { .. } | Ask::Details(_) => {}
Ask::Show(_) | Ask::Saved(_) | Ask::Failed { .. } => {}
}
}
@@ -427,7 +408,7 @@ pub fn refuse() {
stop_session();
control::clear_pending();
}
Ask::Saved(_) | Ask::Failed { .. } | Ask::Details(_) => {}
Ask::Saved(_) | Ask::Failed { .. } => {}
}
}
@@ -458,7 +439,6 @@ pub fn status() -> &'static str {
Some(Ask::Show(_)) => "show",
Some(Ask::Saved(_)) => "saved",
Some(Ask::Failed { .. }) => "failed",
Some(Ask::Details(_)) => "details",
}
}
@@ -511,9 +491,6 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
let colors = theme.surfaces();
let name = &request.name;
let popup_w = if matches!(request.ask, Ask::Details(_)) { DETAILS_W } else { POPUP_W };
let inner_w = popup_w - 2.0 * POPUP_PAD;
let (title, question) = match &request.ask {
Ask::Password => (format!("Join {name}"), "Enter its Wi-Fi password"),
Ask::Pin => (format!("Pair {name}"), "Enter its PIN"),
@@ -523,12 +500,10 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
Ask::Show(_) => (format!("Pair {name}"), "Type this code on it, then press Enter"),
Ask::Saved(_) => (name.clone(), "Its saved Wi-Fi password"),
Ask::Failed { title, .. } => (title.clone(), "What went wrong"),
Ask::Details(_) => (format!("Ethernet · {name}"), "Click an address to copy it"),
};
let mark = match (&request.ask, request.wifi) {
(Ask::Failed { .. }, _) => Glyph::Muted,
(Ask::Details(_), _) => Glyph::Ethernet,
(_, true) => Glyph::Lock,
(_, false) => Glyph::Bluetooth,
};
@@ -540,7 +515,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
Text::new(question).size(11.0).color(theme.dim).elide(),
])
.gap(3.0)
.width(inner_w - HEADER_H - 12.0),
.width(INNER_W - HEADER_H - 12.0),
])
.gap(12.0)
.align(Center);
@@ -566,7 +541,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
rows.push(Box::new(
Rectangle::new()
.width(inner_w)
.width(INNER_W)
.height(FIELD_H)
.radius(10.0)
.fill(CHIP)
@@ -587,22 +562,15 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
let password = password.clone();
// a click on it copies it too
rows.push(Box::new(big_line(&password, 18.0, theme.text).cursor(Pointer).on_click(move |_| copy(&password, 0))));
rows.push(Box::new(big_line(&password, 18.0, theme.text).cursor(Pointer).on_click(move |_| copy(&password, || Prompt::write().copied = Some(0)))));
body_h = CODE_H;
}
Ask::Failed { reason, .. } => {
rows.push(Box::new(Rectangle::new().width(inner_w).height(LINE_H).align_child(Start, Center).child(Text::new(reason.as_str()).size(11.5).color(theme.text).elide())));
rows.push(Box::new(Rectangle::new().width(INNER_W).height(LINE_H).align_child(Start, Center).child(Text::new(reason.as_str()).size(11.5).color(theme.text).elide())));
body_h = LINE_H;
}
Ask::Details(details) => {
let lines: Vec<Box<dyn Widget>> = details.iter().enumerate().map(|(index, (label, value, copy_text))| Box::new(detail(&prompt, inner_w, index, label, value, copy_text.clone())) as Box<dyn Widget>).collect();
body_h = details.len() as f32 * DETAIL_H;
rows.push(Box::new(Column::new(lines).width(inner_w).height(body_h)));
}
Ask::Confirm(_) | Ask::Show(_) => {}
}
@@ -615,15 +583,15 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
let password = password.clone();
let label = if prompt.copied.is_some() { "Copied" } else { "Copy" };
buttons.push(Box::new(button(label, Color::blend(CHIP_OPEN, CHIP_HOVER, lit(0)), theme.text, lit(0)).cursor(Pointer).on_hover(hovering(0)).on_click(move |_| copy(&password, 0))));
} else if !matches!(request.ask, Ask::Failed { .. } | Ask::Details(_)) {
buttons.push(Box::new(button(label, Color::blend(CHIP_OPEN, CHIP_HOVER, lit(0)), theme.text, lit(0)).cursor(Pointer).on_hover(hovering(0)).on_click(move |_| copy(&password, || Prompt::write().copied = Some(0)))));
} else if !matches!(request.ask, Ask::Failed { .. }) {
buttons.push(Box::new(button("Cancel", Color::blend(CHIP_OPEN, CHIP_HOVER, lit(0)), theme.text, lit(0)).cursor(Pointer).on_hover(hovering(0)).on_click(|_| refuse())));
}
let primary = match request.ask {
Ask::Password => Some("Join"),
Ask::Show(_) => None,
Ask::Saved(_) | Ask::Details(_) => Some("Close"),
Ask::Saved(_) => Some("Close"),
Ask::Failed { .. } => Some("OK"),
_ => Some("Pair"),
};
@@ -648,31 +616,27 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
buttons.push(Box::new(primary));
}
rows.push(Box::new(Row::new(vec![Box::new(Row::new(buttons).gap(10.0)) as Box<dyn Widget>]).justify(End).width(inner_w).height(BUTTON_H)));
rows.push(Box::new(Row::new(vec![Box::new(Row::new(buttons).gap(10.0)) as Box<dyn Widget>]).justify(End).width(INNER_W).height(BUTTON_H)));
let parts = if body_h > 0.0 { 3.0 } else { 2.0 };
let card_h = 2.0 * POPUP_PAD + HEADER_H + body_h + BUTTON_H + GAP * (parts - 1.0);
let appear = prompt.appear.value();
let content = Rectangle::new().width(popup_w).height(card_h).padding(POPUP_PAD).child(Column::new(rows).gap(GAP));
let content = Rectangle::new().width(POPUP_W).height(card_h).padding(POPUP_PAD).child(Column::new(rows).gap(GAP));
let card = Rectangle::new()
.width(popup_w)
.width(POPUP_W)
.height(card_h)
.radius(22.0)
.fill(backdrop(&colors, 255))
.border(1.0, Color::blend(theme.outline, theme.accent, 0.5))
.shadow(Shadow::drop(theme.accent).opacity(0.45).blur(22.0))
.translate(0.0, 8.0 * (1.0 - appear))
.child(veiled(content, popup_w, card_h, 22.0, appear, &colors));
.child(veiled(content, POPUP_W, card_h, 22.0, appear, &colors));
// over the middle of the map menu, a little below the control centre's top; the details beside the panel, by the Ethernet tile
let (right, top) = if matches!(request.ask, Ask::Details(_)) {
(RIGHT + PANEL_W + 14.0 - SHADOW, TOP_GAP + 40.0 - SHADOW)
} else {
(RIGHT + (MAP_W - popup_w) / 2.0 - SHADOW, TOP_GAP + 110.0 - SHADOW)
};
// over the middle of the map menu, a little below the control centre's top
let (right, top) = (RIGHT + (MAP_W - POPUP_W) / 2.0 - SHADOW, TOP_GAP + 110.0 - SHADOW);
/*
* the keyboard only while there's something to type: the other popups come on their
@@ -681,14 +645,14 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
*/
let typing = matches!(request.ask, Ask::Password | Ask::Pin | Ask::Passkey);
let window = window(popup_w + 2.0 * SHADOW, card_h + 2.0 * SHADOW)
let window = window(POPUP_W + 2.0 * SHADOW, card_h + 2.0 * SHADOW)
.margin(Margin { top: top.max(0.0) as i32, right: right.max(0.0) as i32, bottom: 0, left: 0 })
.keyboard(if typing { Keyboard::Exclusive } else { Keyboard::OnDemand })
.on_key(|key| match key {
Key::Escape => refuse(),
// nothing to type: Enter is the yes, or the close
Key::Enter => {
let answers = Prompt::read().request.as_ref().is_some_and(|request| matches!(request.ask, Ask::Confirm(_) | Ask::Saved(_) | Ask::Failed { .. } | Ask::Details(_)));
let answers = Prompt::read().request.as_ref().is_some_and(|request| matches!(request.ask, Ask::Confirm(_) | Ask::Saved(_) | Ask::Failed { .. }));
if answers {
answer("");
@@ -697,53 +661,11 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
_ => {}
});
let region = InputArea { x: SHADOW as i32, y: SHADOW as i32, width: popup_w as i32, height: card_h.ceil() as i32 };
let region = InputArea { x: SHADOW as i32, y: SHADOW as i32, width: POPUP_W as i32, height: card_h.ceil() as i32 };
window.input_region(vec![region]).child(Rectangle::new().width(Parent).height(Parent).padding(SHADOW).align_child(Start, Start).child(card))
}
// one line of the details: label, value, and on a row with something to copy, a Copy chip (the whole row copies)
fn detail(prompt: &Prompt, inner_w: f32, index: usize, label: &str, value: &str, copy_text: Option<String>) -> Rectangle {
let theme = theme::now();
let spot = 10 + index as u8;
let lit = prompt.hover.get(&spot).map_or(0.0, Animation::value);
let chip_w = 58.0;
let value_w = inner_w - LABEL_W - chip_w - 16.0;
let chip: Box<dyn Widget> = match &copy_text {
Some(_) => {
let copied = prompt.copied == Some(index);
let ink = if copied { theme.accent } else { Color::blend(theme.dim, theme.text, lit) };
Box::new(Rectangle::new().width(chip_w).height(20.0).radius(6.0).fill(Color::blend(CHIP, CHIP_HOVER, lit)).align_child(Center, Center).child(Text::new(if copied { "Copied" } else { "Copy" }).size(10.0).color(ink)))
}
None => Box::new(Rectangle::new().width(chip_w).height(20.0)),
};
let line = Rectangle::new()
.width(inner_w)
.height(DETAIL_H)
.radius(8.0)
.fill(Color::blend(CLEAR, CHIP_HOVER, 0.6 * lit))
.padding(Padding { top: 0.0, right: 4.0, bottom: 0.0, left: 6.0 })
.align_child(Start, Center)
.child(
Row::new(vec![
Box::new(Rectangle::new().width(LABEL_W).height(DETAIL_H).align_child(Start, Center).child(Text::new(label).size(10.5).color(theme.dim))) as Box<dyn Widget>,
Box::new(Rectangle::new().width(value_w).height(DETAIL_H).align_child(Start, Center).child(Text::new(value).size(11.5).color(theme.text).elide())),
chip,
])
.gap(6.0)
.align(Center),
);
match copy_text {
Some(text) => line.cursor(Pointer).on_hover(hovering(spot)).on_click(move |_| copy(&text, index)),
None => line,
}
}
// a code spaced in threes, the way devices show it
fn spaced(code: &str) -> String {
code.chars().enumerate().flat_map(|(index, digit)| (index > 0 && index % 3 == 0).then_some(' ').into_iter().chain(std::iter::once(digit))).collect()
+4 -4
View File
@@ -47,10 +47,10 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
let hover = Hover::read();
// hidden on a fullscreen monitor until wanted, or for a moment on a workspace change (edge.rs)
let (slide, switched) = {
let (slide, switched, peeking) = {
let edge = Edge::read();
(edge::hidden_by(edge.part(Part::Workspaces, &monitor.name), 0.0, HIDE_H), edge.switched(&monitor.name))
(edge::hidden_by(edge.part(Part::Workspaces, &monitor.name), 0.0, HIDE_H), edge.switched(&monitor.name), edge.shown(&monitor.name) == 0.0)
};
// special workspaces (scratchpads) aren't places to switch to
@@ -81,8 +81,8 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
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);
// the tray's pill right of this one (tray.rs), when an app has an icon there; a peek at a workspace change shows the workspaces alone
let tray = if peeking { None } else { 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);