//! The hosts page: saved (trusted/paired) hosts and live mDNS discovery as tap-to-connect //! tiles in a responsive grid, with a per-host "…" menu (connect / speed test / rename / //! forget) and a manual connect entry — the same card layout as the Linux and Apple clients. use super::connect::{initiate, wake_and_connect}; use super::speed::SpeedState; use super::style::*; use super::{Screen, Svc, Target}; use crate::discovery::DiscoveredHost; use crate::trust::KnownHosts; use windows_reactor::*; /// Overflow-menu item labels — `on_item_clicked` reports the clicked item by its text. const MENU_CONNECT: &str = "Connect"; const MENU_SPEED: &str = "Test network speed\u{2026}"; const MENU_WAKE: &str = "Wake host"; const MENU_RENAME: &str = "Rename\u{2026}"; const MENU_FORGET: &str = "Forget\u{2026}"; /// Tile-grid metrics: minimum tile width before dropping a column, and the gap between tiles. const TILE_MIN_WIDTH: f64 = 320.0; const TILE_GAP: f64 = 12.0; /// Props for the hosts page: the services plus the changing discovery/status data that must /// drive its re-render (compared by value, so a new host list or error refreshes the page). /// /// `forget` and `rename` are the per-host action state, and they live in ROOT (not this page's /// own `use_state`) on purpose: the "…" overflow is a WinUI `MenuFlyout`, whose item clicks are /// wired directly in the reactor backend (`add_Click`) and so bypass the normal event-dispatch /// flush — a *sync* child `SetState` from that handler marks state dirty but never pumps the /// reconciler, so nothing re-renders. Root `AsyncSetState` re-renders the whole tree; because /// these values are props, the changed value propagates back into this page (a child's own async /// state would be memoised away when its props are unchanged). `(fp_hex, _)` in each identifies /// the target saved host; `rename`'s second field is the in-progress draft name. #[derive(Clone)] pub(crate) struct HostsProps { pub(crate) svc: Svc, pub(crate) hosts: Vec, pub(crate) status: String, pub(crate) forget: Option<(String, String)>, pub(crate) rename: Option<(String, String)>, /// Whether the "Add host" modal is open. Root state (like `forget`/`rename`), not the page's /// own `use_state`: a child component's sync `SetState` marks its slot dirty but does not /// re-render when its props are otherwise unchanged, so the toggle wouldn't take. pub(crate) show_add: bool, /// The modal's entrance-tween progress (0 → 1, root-driven): opacity + slide-up offset. pub(crate) add_anim: f64, /// The hovered tile's stable id (saved: fp_hex, discovered: `addr:port`) — root state because /// the pointer enter/exit handlers bypass the reconciler flush, like the flyout clicks above. pub(crate) hover: Option, pub(crate) set_forget: AsyncSetState>, pub(crate) set_rename: AsyncSetState>, pub(crate) set_show_add: AsyncSetState, pub(crate) set_hover: AsyncSetState>, } impl PartialEq for HostsProps { fn eq(&self, other: &Self) -> bool { // Setters are identity-stable; only the value fields drive re-render. self.svc == other.svc && self.hosts == other.hosts && self.status == other.status && self.forget == other.forget && self.rename == other.rename && self.show_add == other.show_add && self.add_anim == other.add_anim && self.hover == other.hover } } /// A host tile. The tap-to-connect summary (monogram, name, address, status row) and the /// optional "…" menu button are SIBLINGS overlaid in one grid cell, never nested: WinUI bubbles /// `Tapped` out of buttons (reactor doesn't mark it handled), so a button inside the tap target /// would fire both its own click and the tile's connect (the old forget-also-connects bug). /// /// Hover renders the WinUI card pointer-over look — the card background lifts to the control /// hover fill while the pointer is inside the tile (tracked via `hover`, see `HostsProps`). fn host_tile( id: &str, hover: &Hover, name: &str, sub: &str, status_row: Element, menu: Option