From 4e235cbac6a380fa000ce2190845c2f35f1e5c8c Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Wed, 14 Oct 2026 13:50:05 +0100 Subject: [PATCH] Keep device data paths on one line The tray used a fixed sample for the data-path column, so longer relay names wrapped and made device rows uneven. Measure each rendered path and retain the widest value, then render path labels without wrapping. Replace direct/via words with embedded Phosphor route icons while preserving transport, relay name, hop count and explanatory tooltips. The app already ships the font; keep its setup in one function and verify registration alongside a long-relay-width regression test. No wire or stored-state changes. Checks: cargo fmt --all -- --check; cargo clippy --locked --workspace --all-targets -- -D warnings; cargo test --locked --workspace --all-targets. --- crates/tsunagi-tray/src/app.rs | 27 ++++++++- crates/tsunagi-tray/src/devices.rs | 89 +++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/crates/tsunagi-tray/src/app.rs b/crates/tsunagi-tray/src/app.rs index c475949..040d20b 100644 --- a/crates/tsunagi-tray/src/app.rs +++ b/crates/tsunagi-tray/src/app.rs @@ -15,6 +15,12 @@ use crate::devices; use crate::stats::Traffic; use crate::ui::{self, UiState}; +fn app_fonts() -> egui::FontDefinitions { + let mut fonts = egui::FontDefinitions::default(); + egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); + fonts +} + /// The window's state. pub(crate) struct App { agent: AgentClient, @@ -34,9 +40,7 @@ impl App { let ctx = cc.egui_ctx.clone(); // Add the Phosphor icon glyphs to the font set so the icon buttons // render instead of showing as tofu. - let mut fonts = egui::FontDefinitions::default(); - egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); - ctx.set_fonts(fonts); + ctx.set_fonts(app_fonts()); let agent = AgentClient::spawn(&runtime, ctx); Ok(Self { @@ -143,3 +147,20 @@ impl App { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn route_icons_have_an_embedded_font_in_the_app() { + let fonts = app_fonts(); + assert!(fonts.font_data.contains_key("phosphor")); + assert!( + fonts + .families + .get(&egui::FontFamily::Proportional) + .is_some_and(|family| family.iter().any(|name| name == "phosphor")) + ); + } +} diff --git a/crates/tsunagi-tray/src/devices.rs b/crates/tsunagi-tray/src/devices.rs index 9076b87..0404626 100644 --- a/crates/tsunagi-tray/src/devices.rs +++ b/crates/tsunagi-tray/src/devices.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use eframe::egui; +use egui_phosphor::regular as icon; use tsunagi::ipc::{DataPathReport, NetworkReport, OverlayPeerReport}; @@ -469,7 +470,7 @@ const COLUMN_SAMPLES: [&str; 10] = [ "end-to-end", "999d / 9999 ms", "255.255.255.255", - "direct over tcp-tls", + "data path", "99999/99999", "1023.9 MB / 1023.9 MB", "1023.9 kB/s", @@ -602,6 +603,14 @@ fn device_table( .x }); widths[0] = widths[0].max(natural + 24.0); + let (path, _) = data_path_text(device.path, device.via_name.as_deref(), device.online); + let path_width = ui.fonts(|fonts| { + fonts + .layout_no_wrap(path, font.clone(), egui::Color32::WHITE) + .size() + .x + }); + widths[5] = widths[5].max(path_width); } ui.memory_mut(|m| m.data.insert_temp(widths_id, widths)); let widths = stretch_columns(widths, ui.available_width()); @@ -812,7 +821,8 @@ fn tunnel_cell(ui: &mut egui::Ui, device: &Device<'_>) { ui.label(protocol).on_hover_text(tunnel_hint(protocol)); } -/// Current route text and whether that route is the preferred direct form. +/// Compact route label and whether that route is the preferred direct form. +/// The icon font is embedded by egui-phosphor and installed in `App::new`. fn data_path_text( path: Option<&DataPathReport>, via_name: Option<&str>, @@ -822,9 +832,13 @@ fn data_path_text( return ("offline".into(), false); } match path { - Some(DataPathReport::Direct { transport }) => (format!("direct over {transport}"), true), + Some(DataPathReport::Direct { transport }) => (format!("{} {transport}", icon::LINK), true), Some(DataPathReport::Relayed { hops, .. }) => ( - format!("via {} · {hops} hops", via_name.unwrap_or("another device")), + format!( + "{} {} · {hops} hops", + icon::SHARE_NETWORK, + via_name.unwrap_or("another device") + ), false, ), Some(DataPathReport::None) | None => ("no data path yet".into(), false), @@ -838,9 +852,12 @@ fn data_path_text( fn data_path_cell(ui: &mut egui::Ui, device: &Device<'_>) { let (text, good) = data_path_text(device.path, device.via_name.as_deref(), device.online); let response = if good { - ui.label(text) + ui.add(egui::Label::new(text).wrap_mode(egui::TextWrapMode::Extend)) } else { - ui.label(egui::RichText::new(text).color(AMBER)) + ui.add( + egui::Label::new(egui::RichText::new(text).color(AMBER)) + .wrap_mode(egui::TextWrapMode::Extend), + ) }; response.on_hover_text(match device.path { Some(DataPathReport::Direct { .. }) => "Direct transport; no mesh relay.", @@ -940,7 +957,7 @@ mod tests { None, true, ), - ("direct over tcp-tls".into(), true) + (format!("{} tcp-tls", icon::LINK), true) ); assert_eq!( tunnel_hint("tcp-tls"), @@ -960,7 +977,7 @@ mod tests { Some("iris"), true, ), - ("via iris · 2 hops".into(), false) + (format!("{} iris · 2 hops", icon::SHARE_NETWORK), false) ); assert_eq!( data_path_text(Some(&DataPathReport::None), None, true), @@ -1017,6 +1034,62 @@ mod tests { ); } + #[test] + fn a_long_relay_name_gets_its_full_column_width() { + let mut relay = member("near", true, DataPathReport::None); + relay.hostname = Some("a-relay-with-a-long-device-name".into()); + let network = NetworkReport { + network_id: "network".into(), + members: vec![ + relay, + member( + "far", + true, + DataPathReport::Relayed { + hops: 2, + via: "near".into(), + }, + ), + ], + ..Default::default() + }; + let rows = devices(&network, "own"); + let far = rows.iter().find(|device| device.id == "far").unwrap(); + let (label, _) = data_path_text(far.path, far.via_name.as_deref(), far.online); + let context = egui::Context::default(); + let mut fonts = egui::FontDefinitions::default(); + egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular); + context.set_fonts(fonts); + let _ = context.run(egui::RawInput::default(), |ctx| { + egui::CentralPanel::default().show(ctx, |ui| { + device_table( + ui, + Unit::Bytes, + &network, + &rows, + &Traffic::default(), + &mut None, + &mut None, + ); + let stored: [f32; 10] = ui + .memory(|memory| { + memory + .data + .get_temp(egui::Id::new(("devices column widths", "network"))) + }) + .unwrap(); + let font = egui::TextStyle::Body.resolve(ui.style()); + let needed = ui.fonts(|fonts| { + fonts + .layout_no_wrap(label.clone(), font, egui::Color32::WHITE) + .size() + .x + }); + assert!(stored[5] >= needed); + }); + }); + } + #[test] fn rendered_table_uses_window_width_without_an_expanding_device_cell() { let context = egui::Context::default();