control centre: Wi-Fi and Bluetooth menus as a map around this desktop
The menus are 820 px wide (the panel resizes to them). This desktop is a bubble in the middle, networks and devices float around it, nearer the stronger (or the more connected) they are, rings ripple out while scanning. Each is tied to the bubble by a line: grey and wobbling while idle, running out from the bubble after a click until it's connected, solid with a travelling pulse once connected. The map is one Canvas: node discs and icons are shapes in it (glyph_shapes), the node widgets only take the pointer. Seven icon Canvas widgets had cost 4.6 ms of gpu a frame and halved the frame rate; now 1.8 ms at 60 fps. The menu window is sized to its menu.
This commit is contained in:
+191
-87
@@ -17,6 +17,7 @@ use amane::{
|
||||
|
||||
use crate::glyph::{Glyph, icon};
|
||||
use crate::island::{self, CHIP, CHIP_HOVER, CHIP_OPEN, CLEAR, COMPACT_H, HOVER_MS, PILL_GONE, SHADOW, SHAPE_DONE, TOP_GAP, TRACK, backdrop, faded, mix, phases, veiled};
|
||||
use crate::map::{self, AREA_H, Link, MAP_W, NODES, Node};
|
||||
use crate::parse;
|
||||
use crate::sound::Sound;
|
||||
use crate::system::{self, Charge, Gpu, Links, Power, VpnState};
|
||||
@@ -38,6 +39,9 @@ const OUTPUTS: usize = 8;
|
||||
// the pill and the panel sit this far from the monitor's right edge
|
||||
const RIGHT: f32 = 12.0;
|
||||
|
||||
// both windows are as wide as a map menu, the widest the panel becomes
|
||||
const WINDOW_W: f32 = RIGHT + MAP_W + SHADOW;
|
||||
|
||||
// the pill is as wide as its icons, PILL_PAD on each side, PILL_GAP apart
|
||||
const PILL_PAD: f32 = 16.0;
|
||||
const PILL_GAP: f32 = 12.0;
|
||||
@@ -141,6 +145,9 @@ pub struct Control {
|
||||
// the pointer is over the panel's window or the menu's; it closes only once it has left both
|
||||
inside_panel: bool,
|
||||
inside_menu: bool,
|
||||
|
||||
// a network or device just clicked to connect (iwd path or bluez path): its line runs out until it's connected
|
||||
pending: Option<(String, Instant)>,
|
||||
}
|
||||
|
||||
impl Service for Control {
|
||||
@@ -158,6 +165,7 @@ impl Service for Control {
|
||||
menu_open: menu_opening(),
|
||||
inside_panel: false,
|
||||
inside_menu: false,
|
||||
pending: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +192,11 @@ impl Control {
|
||||
fn busy(&self, tile: Tile, subtitle: &str) -> bool {
|
||||
self.busy.get(&tile).is_some_and(|(at, before)| before == subtitle && at.elapsed() < Duration::from_secs(10))
|
||||
}
|
||||
|
||||
// asked to connect less than 20 s ago
|
||||
fn pending(&self, id: &str) -> bool {
|
||||
self.pending.as_ref().is_some_and(|(pending, at)| pending == id && at.elapsed() < Duration::from_secs(20))
|
||||
}
|
||||
}
|
||||
|
||||
// one spot lit at a time: amane matches hover targets by their place in the frame, so a vanished one never hears the pointer leave
|
||||
@@ -290,7 +303,8 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
||||
let hovered_here = control.pointer_on.as_deref() == Some(here);
|
||||
let lift = if hovered_here { control.lit(Spot::Pill) } else { 0.0 } * (1.0 - grow);
|
||||
|
||||
let window_w = RIGHT + PANEL_W + SHADOW;
|
||||
// as wide as the widest thing it can become, a map menu; the shape grows leftwards from the corner
|
||||
let window_w = WINDOW_W;
|
||||
|
||||
let window = LayerWindow::new()
|
||||
.width(window_w)
|
||||
@@ -333,21 +347,25 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
||||
*/
|
||||
let menu_t = if opened_here { control.menu_open.value() } else { 0.0 };
|
||||
|
||||
let menu_h = match (control.menu.filter(|_| menu_t > 0.0), &sound) {
|
||||
(Some(menu), Some(sound)) => Some(menu_height(&menu_content(menu, &control, &links, &bluetooth, sound, layout.extra_output).0)),
|
||||
let menu_size = match (control.menu.filter(|_| menu_t > 0.0), &sound) {
|
||||
(Some(menu), Some(sound)) => {
|
||||
let content = menu_content(menu, &control, &links, &bluetooth, sound, layout.extra_output).0;
|
||||
|
||||
Some((content.width, menu_height(&content)))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let fade = if menu_h.is_some() { 1.0 - (menu_t / PILL_GONE).clamp(0.0, 1.0) } else { 1.0 };
|
||||
let fade = if menu_size.is_some() { 1.0 - (menu_t / PILL_GONE).clamp(0.0, 1.0) } else { 1.0 };
|
||||
|
||||
let morph = ((menu_t - PILL_GONE) / (SHAPE_DONE - PILL_GONE)).clamp(0.0, 1.0);
|
||||
let morph = morph * morph * (3.0 - 2.0 * morph);
|
||||
|
||||
let full_h = menu_h.map_or(layout.height(), |menu_h| mix(layout.height(), menu_h, morph));
|
||||
let (full_w, full_h) = menu_size.map_or((PANEL_W, layout.height()), |(menu_w, menu_h)| (mix(PANEL_W, menu_w, morph), mix(layout.height(), menu_h, morph)));
|
||||
|
||||
let (icons, pill_w) = pill_items(&links, &bluetooth, &power);
|
||||
|
||||
let width = mix(pill_w + PILL_HOVER_GROW * lift, PANEL_W, grow);
|
||||
let width = mix(pill_w + PILL_HOVER_GROW * lift, full_w, grow);
|
||||
let height = mix(COMPACT_H, full_h, grow);
|
||||
let radius = mix(COMPACT_H / 2.0, PANEL_RADIUS, grow);
|
||||
|
||||
@@ -1393,17 +1411,16 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
||||
|
||||
let control = Control::read();
|
||||
|
||||
let window_w = RIGHT + PANEL_W + SHADOW;
|
||||
let largest = Layout::LARGEST.height();
|
||||
|
||||
let window = LayerWindow::new()
|
||||
.width(window_w)
|
||||
.height(TOP_GAP + largest + SHADOW)
|
||||
.anchor_vertical(Vertical::Top)
|
||||
.anchor_horizontal(Horizontal::Right)
|
||||
.layer(Layer::Overlay)
|
||||
.space(Zone::Ignore)
|
||||
.namespace("magic-control-menu");
|
||||
let window = |width: f32, height: f32| {
|
||||
LayerWindow::new()
|
||||
.width(width)
|
||||
.height(height)
|
||||
.anchor_vertical(Vertical::Top)
|
||||
.anchor_horizontal(Horizontal::Right)
|
||||
.layer(Layer::Overlay)
|
||||
.space(Zone::Ignore)
|
||||
.namespace("magic-control-menu")
|
||||
};
|
||||
|
||||
let here = monitor.name.as_str();
|
||||
let on_here = control.opened_on.as_deref().is_none_or(|name| name == here);
|
||||
@@ -1413,9 +1430,9 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
||||
let leaving = !control.menu_up && !control.expanded;
|
||||
let (_, _, panel_reveal) = phases(if on_here { control.open.value() } else { 0.0 });
|
||||
|
||||
// never hidden (see the island's view): an empty click-through window instead
|
||||
// never hidden (see the island's view): a tiny, empty click-through window instead
|
||||
let Some(menu) = control.menu.filter(|_| on_here && timeline > 0.0 && (!leaving || panel_reveal > 0.0)) else {
|
||||
return window.click_through().child(Rectangle::new().width(0.0).height(0.0));
|
||||
return window(1.0, 1.0).click_through().child(Rectangle::new().width(0.0).height(0.0));
|
||||
};
|
||||
|
||||
let links = Links::read();
|
||||
@@ -1425,40 +1442,60 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
||||
|
||||
let (content, origin) = menu_content(menu, &control, &links, &bluetooth, &sound, extra);
|
||||
|
||||
let menu_w = content.width;
|
||||
let menu_h = menu_height(&content);
|
||||
|
||||
// the maps float and wobble all the time: frames for as long as one shows
|
||||
if matches!(menu, Menu::Wifi | Menu::Bluetooth) {
|
||||
amane::request_frame();
|
||||
}
|
||||
|
||||
let (grow, _, settled) = phases(timeline);
|
||||
|
||||
// leaving: the content fades with the panel's own timeline, the shape stays put on the panel
|
||||
let reveal = if leaving { settled * panel_reveal } else { settled };
|
||||
|
||||
// from the tile, measured from the panel's top left, which sits at the right of the menu-wide box
|
||||
let (x0, y0, w0, h0) = origin;
|
||||
let x0 = x0 + menu_w - PANEL_W;
|
||||
|
||||
// only as big as this menu and the tile it grows from: every frame redraws the whole window, and maps redraw all the time
|
||||
let box_h = menu_h.max(y0 + h0);
|
||||
let window_w = RIGHT + menu_w + SHADOW;
|
||||
let window = window(window_w, TOP_GAP + box_h + SHADOW);
|
||||
|
||||
// to the menu's own size, against the same right edge as the panel
|
||||
let (x, y) = (mix(x0, 0.0, grow), mix(y0, 0.0, grow));
|
||||
let (width, height) = (mix(w0, PANEL_W, grow), mix(h0, menu_h, grow));
|
||||
let (width, height) = (mix(w0, menu_w, grow), mix(h0, menu_h, grow));
|
||||
let radius = mix(MODULE_RADIUS, PANEL_RADIUS, grow);
|
||||
|
||||
let colors = theme::now().surfaces();
|
||||
|
||||
let face = if reveal > 0.0 {
|
||||
let page = Rectangle::new()
|
||||
.width(PANEL_W)
|
||||
.width(menu_w)
|
||||
.height(menu_h)
|
||||
.padding(MENU_PAD)
|
||||
.child(Column::new(children![menu_header(&control, content.title, content.right), content.body]).gap(GAP));
|
||||
.child(Column::new(children![menu_header(&control, content.title, content.right, menu_w - 2.0 * MENU_PAD), content.body]).gap(GAP));
|
||||
|
||||
veiled(page, width, height, radius, reveal, &colors)
|
||||
} else {
|
||||
Rectangle::new().width(0.0).height(0.0)
|
||||
};
|
||||
|
||||
// a shadow only while it grows: settled, it lies on the panel, which has its own glow; a blur costs every frame, even at 0
|
||||
let shadow = 0.45 * grow * (1.0 - settled);
|
||||
|
||||
let shape = Rectangle::new()
|
||||
.width(width)
|
||||
.height(height)
|
||||
.radius(radius)
|
||||
.fill(backdrop(&colors, 255))
|
||||
.border(1.0, theme::now().outline)
|
||||
// a shadow only while it grows: settled, it lies on the panel, which has its own glow
|
||||
.shadow(Shadow::drop("#000000").opacity(0.45 * grow * (1.0 - settled)).blur(24.0).offset(0.0, 10.0))
|
||||
.border(1.0, theme::now().outline);
|
||||
|
||||
let shape = if shadow > 0.01 { shape.shadow(Shadow::drop("#000000").opacity(shadow).blur(24.0).offset(0.0, 10.0)) } else { shape };
|
||||
|
||||
let shape = shape
|
||||
.align_child(Center, Start)
|
||||
.translate(x, y)
|
||||
.on_hover(|inside| {
|
||||
@@ -1471,7 +1508,7 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
||||
.child(face);
|
||||
|
||||
let region = InputArea {
|
||||
x: (window_w - RIGHT - PANEL_W + x).floor() as i32,
|
||||
x: (window_w - RIGHT - menu_w + x).floor() as i32,
|
||||
y: (TOP_GAP + y).floor() as i32,
|
||||
width: width.ceil() as i32,
|
||||
height: height.ceil() as i32,
|
||||
@@ -1483,7 +1520,7 @@ pub fn menu_view(monitor: &Monitor) -> LayerWindow {
|
||||
.height(Parent)
|
||||
.padding(Padding { top: TOP_GAP, right: RIGHT, bottom: 0.0, left: 0.0 })
|
||||
.align_child(End, Start)
|
||||
.child(Rectangle::new().width(PANEL_W).height(largest).align_child(Start, Start).child(shape)),
|
||||
.child(Rectangle::new().width(menu_w).height(box_h).align_child(Start, Start).child(shape)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1517,10 +1554,11 @@ fn menu_height(content: &MenuContent) -> f32 {
|
||||
(2.0 * MENU_PAD + MENU_HEADER_H + GAP + content.height).min(Layout::LARGEST.height())
|
||||
}
|
||||
|
||||
// what a menu shows under its header
|
||||
// what a menu shows under its header, and how wide the menu is (the panel's width, or a map's)
|
||||
struct MenuContent {
|
||||
title: &'static str,
|
||||
right: Option<Box<dyn Widget>>,
|
||||
width: f32,
|
||||
height: f32,
|
||||
body: Column,
|
||||
}
|
||||
@@ -1550,14 +1588,14 @@ impl Body {
|
||||
self.add(Rectangle::new().width(MENU_W).height(NOTE_H).padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 4.0 }).align_child(Start, Center).child(note(text)), NOTE_H);
|
||||
}
|
||||
|
||||
fn into_content(self, title: &'static str, right: Option<Box<dyn Widget>>) -> MenuContent {
|
||||
fn into_content(self, title: &'static str, right: Option<Box<dyn Widget>>, width: f32) -> MenuContent {
|
||||
let gap = self.gap;
|
||||
|
||||
MenuContent { title, right, height: self.height, body: Column::new(self.rows).gap(gap) }
|
||||
MenuContent { title, right, width, height: self.height, body: Column::new(self.rows).gap(gap) }
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_header(control: &Control, title: &str, right: Option<Box<dyn Widget>>) -> Row {
|
||||
fn menu_header(control: &Control, title: &str, right: Option<Box<dyn Widget>>, width: f32) -> Row {
|
||||
let theme = theme::now();
|
||||
|
||||
let lit = control.lit(Spot::Back);
|
||||
@@ -1580,7 +1618,7 @@ fn menu_header(control: &Control, title: &str, right: Option<Box<dyn Widget>>) -
|
||||
parts.push(right);
|
||||
}
|
||||
|
||||
Row::new(parts).justify(SpaceBetween).align(Center).width(MENU_W).height(MENU_HEADER_H)
|
||||
Row::new(parts).justify(SpaceBetween).align(Center).width(width).height(MENU_HEADER_H)
|
||||
}
|
||||
|
||||
// an on/off switch; grey: the service behind it isn't running, and switching on starts it
|
||||
@@ -1680,59 +1718,86 @@ fn wifi_menu(control: &Control, links: &Links) -> MenuContent {
|
||||
Box::new(toggle) as Box<dyn Widget>
|
||||
});
|
||||
|
||||
let mut body = Body::new(4.0);
|
||||
|
||||
if !wifi.service {
|
||||
body.note("iwd isn't running. The switch starts it.");
|
||||
let note = if !wifi.service {
|
||||
Some("iwd isn't running. The switch starts it.")
|
||||
} else if wifi.device.is_empty() {
|
||||
body.note("iwd sees no Wi-Fi device.");
|
||||
Some("iwd sees no Wi-Fi device.")
|
||||
} else if wifi.access_point {
|
||||
body.note("The hotspot is using Wi-Fi; the switch stops it.");
|
||||
Some("The hotspot is using Wi-Fi; the switch stops it.")
|
||||
} else if !wifi.powered {
|
||||
body.note("Wi-Fi is off.");
|
||||
Some("Wi-Fi is off.")
|
||||
} else if wifi.networks.is_empty() {
|
||||
body.note("Looking for networks…");
|
||||
Some("Looking for networks…")
|
||||
} else {
|
||||
for (index, network) in wifi.networks.iter().take(MENU_ROWS).enumerate() {
|
||||
let subtitle = if network.connected {
|
||||
"Connected"
|
||||
} else if network.known {
|
||||
"Saved"
|
||||
} else if network.secured {
|
||||
"New · join it once with iwctl"
|
||||
None
|
||||
};
|
||||
|
||||
// by name, so each keeps its place around the bubble while signals change
|
||||
let mut networks: Vec<_> = if note.is_none() { wifi.networks.iter().take(NODES).collect() } else { Vec::new() };
|
||||
|
||||
networks.sort_by(|one, other| one.name.cmp(&other.name));
|
||||
|
||||
let nodes = networks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, network)| {
|
||||
let link = if network.connected {
|
||||
Link::Connected
|
||||
} else if network.connecting || control.pending(&network.path) {
|
||||
Link::Connecting
|
||||
} else {
|
||||
"Open"
|
||||
Link::Idle
|
||||
};
|
||||
|
||||
let lock = network.secured.then(|| Box::new(icon(Glyph::Lock, 12.0, theme::now().dim)) as Box<dyn Widget>);
|
||||
|
||||
let row = menu_row(control, index, Glyph::Wifi(network.bars), network.connected, &network.name, subtitle, lock);
|
||||
let detail = match link {
|
||||
Link::Connected => "Connected",
|
||||
Link::Connecting => "Connecting…",
|
||||
Link::Idle if network.known => "Saved",
|
||||
Link::Idle if !network.secured => "Open",
|
||||
Link::Idle => "New · join with iwctl",
|
||||
};
|
||||
|
||||
let joinable = network.known || !network.secured;
|
||||
let path = network.path.clone();
|
||||
|
||||
let row = if network.connected {
|
||||
row.cursor(Pointer).on_click(|_| later(system::disconnect_wifi, None))
|
||||
} else if joinable {
|
||||
let path = network.path.clone();
|
||||
let on_click: Option<Box<dyn Fn()>> = match link {
|
||||
Link::Connected => Some(Box::new(|| {
|
||||
Control::write().pending = None;
|
||||
|
||||
later(system::disconnect_wifi, None);
|
||||
})),
|
||||
Link::Idle if joinable => Some(Box::new(move || {
|
||||
Control::write().pending = Some((path.clone(), Instant::now()));
|
||||
|
||||
row.cursor(Pointer).on_click(move |_| {
|
||||
let path = path.clone();
|
||||
|
||||
later(move || system::connect_network(&path), None);
|
||||
})
|
||||
} else {
|
||||
row
|
||||
})),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
body.add(row, MENU_ROW_H);
|
||||
}
|
||||
}
|
||||
Node {
|
||||
glyph: Glyph::Wifi(network.bars),
|
||||
name: network.name.clone(),
|
||||
detail: String::from(detail),
|
||||
nearness: f32::from(network.bars.saturating_sub(1)) / 2.0,
|
||||
link,
|
||||
lit: control.lit(Spot::Row(index)),
|
||||
on_hover: Box::new(hovering(Spot::Row(index))),
|
||||
on_click,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut body = Body::new(GAP);
|
||||
|
||||
body.add(map::map(MAP_W - 2.0 * MENU_PAD, AREA_H, &links.host, nodes, wifi.scanning, note), AREA_H);
|
||||
|
||||
if wifi.service {
|
||||
body.add(footer(control, "Stop the Wi-Fi service").on_click(|_| later(system::stop_wifi_service, None)), 30.0);
|
||||
}
|
||||
|
||||
body.into_content("Wi-Fi", right)
|
||||
body.into_content("Wi-Fi", right, MAP_W)
|
||||
}
|
||||
|
||||
fn bluetooth_menu(control: &Control, bluetooth: &Bluetooth, links: &Links) -> MenuContent {
|
||||
@@ -1745,44 +1810,83 @@ fn bluetooth_menu(control: &Control, bluetooth: &Bluetooth, links: &Links) -> Me
|
||||
Box::new(toggle) as Box<dyn Widget>
|
||||
});
|
||||
|
||||
let mut body = Body::new(4.0);
|
||||
|
||||
if !bluetooth.available() && !links.bluetooth_service {
|
||||
body.note("bluez isn't installed.");
|
||||
body.note("emerge net-wireless/bluez, then switch it on here.");
|
||||
let note = if !bluetooth.available() && !links.bluetooth_service {
|
||||
Some("bluez isn't installed: emerge net-wireless/bluez")
|
||||
} else if !bluetooth.available() {
|
||||
body.note("bluetoothd isn't running. The switch starts it.");
|
||||
Some("bluetoothd isn't running. The switch starts it.")
|
||||
} else if !bluetooth.powered() {
|
||||
body.note("Bluetooth is off.");
|
||||
Some("Bluetooth is off.")
|
||||
} else if bluetooth.devices().is_empty() {
|
||||
body.note(if bluetooth.scanning() { "Looking for devices…" } else { "No devices." });
|
||||
Some(if bluetooth.scanning() { "Looking for devices…" } else { "No devices." })
|
||||
} else {
|
||||
for (index, device) in bluetooth.devices().iter().take(MENU_ROWS).enumerate() {
|
||||
let subtitle = match (device.connected(), device.paired(), device.battery()) {
|
||||
(true, _, Some(battery)) => format!("Connected · {battery}%"),
|
||||
(true, _, None) => String::from("Connected"),
|
||||
(false, true, _) => String::from("Paired"),
|
||||
(false, false, _) => String::from("Not paired · click to pair"),
|
||||
None
|
||||
};
|
||||
|
||||
// by address, so each keeps its place around the bubble
|
||||
let mut devices: Vec<_> = if note.is_none() { bluetooth.devices().iter().take(NODES).collect() } else { Vec::new() };
|
||||
|
||||
devices.sort_by(|one, other| one.address().cmp(other.address()));
|
||||
|
||||
let nodes = devices
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, device)| {
|
||||
let link = if device.connected() {
|
||||
Link::Connected
|
||||
} else if control.pending(device.path()) {
|
||||
Link::Connecting
|
||||
} else {
|
||||
Link::Idle
|
||||
};
|
||||
|
||||
let detail = match (link, device.battery()) {
|
||||
(Link::Connected, Some(battery)) => format!("Connected · {battery}%"),
|
||||
(Link::Connected, None) => String::from("Connected"),
|
||||
(Link::Connecting, _) => String::from("Connecting…"),
|
||||
_ if device.paired() => String::from("Paired"),
|
||||
_ => String::from("Not paired · click to pair"),
|
||||
};
|
||||
|
||||
// what it is, as far as bluez can tell
|
||||
let glyph = match device.icon() {
|
||||
kind if kind.starts_with("audio") => Glyph::Speaker,
|
||||
"input-keyboard" => Glyph::Keyboard,
|
||||
_ => Glyph::Bluetooth,
|
||||
};
|
||||
|
||||
let path = String::from(device.path());
|
||||
let (connected, paired) = (device.connected(), device.paired());
|
||||
|
||||
let row = menu_row(control, index, Glyph::Bluetooth, connected, device.name(), &subtitle, None).cursor(Pointer).on_click(move |_| {
|
||||
let on_click: Box<dyn Fn()> = Box::new(move || {
|
||||
if connected {
|
||||
Control::write().pending = None;
|
||||
|
||||
Bluetooth::disconnect(&path);
|
||||
} else if paired {
|
||||
Bluetooth::connect(&path);
|
||||
} else {
|
||||
Bluetooth::pair(&path);
|
||||
Control::write().pending = Some((path.clone(), Instant::now()));
|
||||
|
||||
if paired { Bluetooth::connect(&path) } else { Bluetooth::pair(&path) }
|
||||
}
|
||||
});
|
||||
|
||||
body.add(row, MENU_ROW_H);
|
||||
}
|
||||
}
|
||||
Node {
|
||||
glyph,
|
||||
name: device.name().to_string(),
|
||||
detail,
|
||||
nearness: if connected { 1.0 } else if paired { 0.6 } else { 0.1 },
|
||||
link,
|
||||
lit: control.lit(Spot::Row(index)),
|
||||
on_hover: Box::new(hovering(Spot::Row(index))),
|
||||
on_click: Some(on_click),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
body.into_content("Bluetooth", right)
|
||||
let mut body = Body::new(GAP);
|
||||
|
||||
body.add(map::map(MAP_W - 2.0 * MENU_PAD, AREA_H, &links.host, nodes, bluetooth.available() && bluetooth.scanning(), note), AREA_H);
|
||||
|
||||
body.into_content("Bluetooth", right, MAP_W)
|
||||
}
|
||||
|
||||
fn vpn_menu(control: &Control, links: &Links) -> MenuContent {
|
||||
@@ -1813,7 +1917,7 @@ fn vpn_menu(control: &Control, links: &Links) -> MenuContent {
|
||||
body.add(row, MENU_ROW_H);
|
||||
}
|
||||
|
||||
body.into_content("VPN", None)
|
||||
body.into_content("VPN", None, PANEL_W)
|
||||
}
|
||||
|
||||
// every output with its own volume; a click on its name makes it the default
|
||||
@@ -1860,5 +1964,5 @@ fn sound_menu(control: &Control, sound: &Sound) -> MenuContent {
|
||||
body.add(Column::new(children![line, output_slider(control, sink, MENU_W)]).gap(6.0), OUTPUT_H + 6.0 + SLIDER_H);
|
||||
}
|
||||
|
||||
body.into_content("Sound", None)
|
||||
body.into_content("Sound", None, PANEL_W)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user