| 1 | //! GUI state management |
| 2 | |
| 3 | #![allow(dead_code)] |
| 4 | |
| 5 | use hyprkvm_common::Direction; |
| 6 | use crate::config::{Config, NeighborConfig}; |
| 7 | use std::path::PathBuf; |
| 8 | |
| 9 | /// Grid position in the visual layout |
| 10 | /// Self machine is always at (0, 0) |
| 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 12 | pub struct GridPos { |
| 13 | pub x: i32, |
| 14 | pub y: i32, |
| 15 | } |
| 16 | |
| 17 | impl GridPos { |
| 18 | pub fn origin() -> Self { |
| 19 | Self { x: 0, y: 0 } |
| 20 | } |
| 21 | |
| 22 | pub fn from_direction(direction: Direction) -> Self { |
| 23 | match direction { |
| 24 | Direction::Left => Self { x: -1, y: 0 }, |
| 25 | Direction::Right => Self { x: 1, y: 0 }, |
| 26 | Direction::Up => Self { x: 0, y: -1 }, |
| 27 | Direction::Down => Self { x: 0, y: 1 }, |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | pub fn to_direction(&self) -> Option<Direction> { |
| 32 | match (self.x, self.y) { |
| 33 | (-1, 0) => Some(Direction::Left), |
| 34 | (1, 0) => Some(Direction::Right), |
| 35 | (0, -1) => Some(Direction::Up), |
| 36 | (0, 1) => Some(Direction::Down), |
| 37 | _ => None, // Origin or invalid position |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | /// Check if this is a valid neighbor position (adjacent to origin) |
| 42 | pub fn is_valid_neighbor(&self) -> bool { |
| 43 | self.to_direction().is_some() |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /// Connection status for a machine |
| 48 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 49 | pub enum ConnectionStatus { |
| 50 | #[default] |
| 51 | Disconnected, |
| 52 | Connecting, |
| 53 | Connected, |
| 54 | } |
| 55 | |
| 56 | /// Visual representation of a machine in the GUI |
| 57 | #[derive(Debug, Clone)] |
| 58 | pub struct MachineNode { |
| 59 | /// Machine name |
| 60 | pub name: String, |
| 61 | /// Network address (empty for self) |
| 62 | pub address: String, |
| 63 | /// Position on the visual grid |
| 64 | pub grid_pos: GridPos, |
| 65 | /// Connection status |
| 66 | pub status: ConnectionStatus, |
| 67 | /// Is this the local machine? |
| 68 | pub is_self: bool, |
| 69 | /// Original neighbor index in config (None for self) |
| 70 | pub config_index: Option<usize>, |
| 71 | } |
| 72 | |
| 73 | impl MachineNode { |
| 74 | pub fn new_self(name: String) -> Self { |
| 75 | Self { |
| 76 | name, |
| 77 | address: String::new(), |
| 78 | grid_pos: GridPos::origin(), |
| 79 | status: ConnectionStatus::Connected, // Self is always connected |
| 80 | is_self: true, |
| 81 | config_index: None, |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | pub fn new_neighbor( |
| 86 | name: String, |
| 87 | address: String, |
| 88 | direction: Direction, |
| 89 | config_index: usize, |
| 90 | ) -> Self { |
| 91 | Self { |
| 92 | name, |
| 93 | address, |
| 94 | grid_pos: GridPos::from_direction(direction), |
| 95 | status: ConnectionStatus::Disconnected, |
| 96 | is_self: false, |
| 97 | config_index: Some(config_index), |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | /// Drag state for machine repositioning |
| 103 | #[derive(Debug, Clone, Default)] |
| 104 | pub struct DragState { |
| 105 | /// Index of machine being dragged (None if not dragging) |
| 106 | pub dragging: Option<usize>, |
| 107 | /// Current cursor position during drag |
| 108 | pub cursor_pos: Option<iced::Point>, |
| 109 | /// Snap target (if cursor is near a valid position) |
| 110 | pub snap_target: Option<GridPos>, |
| 111 | } |
| 112 | |
| 113 | impl DragState { |
| 114 | pub fn is_dragging(&self) -> bool { |
| 115 | self.dragging.is_some() |
| 116 | } |
| 117 | |
| 118 | pub fn start(&mut self, machine_index: usize) { |
| 119 | self.dragging = Some(machine_index); |
| 120 | self.cursor_pos = None; |
| 121 | self.snap_target = None; |
| 122 | } |
| 123 | |
| 124 | pub fn update(&mut self, cursor: iced::Point) { |
| 125 | self.cursor_pos = Some(cursor); |
| 126 | } |
| 127 | |
| 128 | pub fn end(&mut self) -> Option<(usize, Option<GridPos>)> { |
| 129 | let result = self.dragging.map(|idx| (idx, self.snap_target)); |
| 130 | self.dragging = None; |
| 131 | self.cursor_pos = None; |
| 132 | self.snap_target = None; |
| 133 | result |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | /// Main GUI state |
| 138 | #[derive(Debug)] |
| 139 | pub struct GuiState { |
| 140 | /// Path to config file |
| 141 | pub config_path: PathBuf, |
| 142 | /// Current configuration |
| 143 | pub config: Config, |
| 144 | /// Machine nodes for rendering |
| 145 | pub machines: Vec<MachineNode>, |
| 146 | /// Drag state |
| 147 | pub drag: DragState, |
| 148 | /// Error message to display |
| 149 | pub error: Option<String>, |
| 150 | /// Success message to display (auto-dismisses) |
| 151 | pub success: Option<String>, |
| 152 | /// Whether daemon restart is needed (shows restart button) |
| 153 | pub needs_restart: bool, |
| 154 | /// Show add machine modal |
| 155 | pub show_add_modal: bool, |
| 156 | /// New machine form data |
| 157 | pub new_machine: NewMachineForm, |
| 158 | } |
| 159 | |
| 160 | /// Form data for adding a new machine |
| 161 | #[derive(Debug, Clone, Default)] |
| 162 | pub struct NewMachineForm { |
| 163 | pub name: String, |
| 164 | pub address: String, |
| 165 | } |
| 166 | |
| 167 | impl GuiState { |
| 168 | /// Create state from config |
| 169 | pub fn from_config(config_path: PathBuf, config: Config) -> Self { |
| 170 | let mut machines = Vec::new(); |
| 171 | |
| 172 | // Add self machine at origin |
| 173 | machines.push(MachineNode::new_self(config.machines.self_name.clone())); |
| 174 | |
| 175 | // Add neighbors at their positions |
| 176 | for (idx, neighbor) in config.machines.neighbors.iter().enumerate() { |
| 177 | machines.push(MachineNode::new_neighbor( |
| 178 | neighbor.name.clone(), |
| 179 | neighbor.address.to_string(), |
| 180 | neighbor.direction, |
| 181 | idx, |
| 182 | )); |
| 183 | } |
| 184 | |
| 185 | Self { |
| 186 | config_path, |
| 187 | config, |
| 188 | machines, |
| 189 | drag: DragState::default(), |
| 190 | error: None, |
| 191 | success: None, |
| 192 | needs_restart: false, |
| 193 | show_add_modal: false, |
| 194 | new_machine: NewMachineForm::default(), |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | /// Find machine at grid position |
| 199 | pub fn machine_at(&self, pos: GridPos) -> Option<usize> { |
| 200 | self.machines.iter().position(|m| m.grid_pos == pos) |
| 201 | } |
| 202 | |
| 203 | /// Get available neighbor positions (not occupied, excluding dragged machine) |
| 204 | pub fn available_positions(&self) -> Vec<GridPos> { |
| 205 | let occupied: std::collections::HashSet<_> = self.machines |
| 206 | .iter() |
| 207 | .enumerate() |
| 208 | .filter(|(idx, _)| Some(*idx) != self.drag.dragging) // Exclude dragged machine |
| 209 | .map(|(_, m)| m.grid_pos) |
| 210 | .collect(); |
| 211 | |
| 212 | [ |
| 213 | GridPos { x: -1, y: 0 }, |
| 214 | GridPos { x: 1, y: 0 }, |
| 215 | GridPos { x: 0, y: -1 }, |
| 216 | GridPos { x: 0, y: 1 }, |
| 217 | ] |
| 218 | .into_iter() |
| 219 | .filter(|pos| !occupied.contains(pos)) |
| 220 | .collect() |
| 221 | } |
| 222 | |
| 223 | /// Move a machine to a new position |
| 224 | pub fn move_machine(&mut self, machine_idx: usize, new_pos: GridPos) -> bool { |
| 225 | // Can't move self |
| 226 | if self.machines[machine_idx].is_self { |
| 227 | return false; |
| 228 | } |
| 229 | |
| 230 | // Check if position is valid |
| 231 | if !new_pos.is_valid_neighbor() { |
| 232 | return false; |
| 233 | } |
| 234 | |
| 235 | // Check if position is occupied by another machine (not the one being moved) |
| 236 | if let Some(occupant_idx) = self.machine_at(new_pos) { |
| 237 | if occupant_idx != machine_idx { |
| 238 | return false; |
| 239 | } |
| 240 | // Same position - no need to update |
| 241 | return true; |
| 242 | } |
| 243 | |
| 244 | // Update the machine position |
| 245 | self.machines[machine_idx].grid_pos = new_pos; |
| 246 | |
| 247 | // Update config |
| 248 | if let Some(config_idx) = self.machines[machine_idx].config_index { |
| 249 | if let Some(direction) = new_pos.to_direction() { |
| 250 | self.config.machines.neighbors[config_idx].direction = direction; |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | true |
| 255 | } |
| 256 | |
| 257 | /// Add a new neighbor machine |
| 258 | pub fn add_machine(&mut self, name: String, address: String, pos: GridPos) -> Result<(), String> { |
| 259 | // Validate position |
| 260 | let direction = pos.to_direction().ok_or("Invalid position")?; |
| 261 | |
| 262 | // Check if position is occupied |
| 263 | if self.machine_at(pos).is_some() { |
| 264 | return Err("Position already occupied".to_string()); |
| 265 | } |
| 266 | |
| 267 | // Parse address |
| 268 | let socket_addr: std::net::SocketAddr = address |
| 269 | .parse() |
| 270 | .map_err(|e| format!("Invalid address: {}", e))?; |
| 271 | |
| 272 | // Add to config |
| 273 | let config_idx = self.config.machines.neighbors.len(); |
| 274 | self.config.machines.neighbors.push(NeighborConfig { |
| 275 | name: name.clone(), |
| 276 | direction, |
| 277 | address: socket_addr, |
| 278 | fingerprint: None, |
| 279 | tls: None, |
| 280 | }); |
| 281 | |
| 282 | // Add to machines list |
| 283 | self.machines.push(MachineNode::new_neighbor( |
| 284 | name, |
| 285 | address, |
| 286 | direction, |
| 287 | config_idx, |
| 288 | )); |
| 289 | |
| 290 | Ok(()) |
| 291 | } |
| 292 | |
| 293 | /// Remove a neighbor machine |
| 294 | pub fn remove_machine(&mut self, machine_idx: usize) -> bool { |
| 295 | // Can't remove self |
| 296 | if machine_idx == 0 || self.machines[machine_idx].is_self { |
| 297 | return false; |
| 298 | } |
| 299 | |
| 300 | // Get config index before removal |
| 301 | let config_idx = match self.machines[machine_idx].config_index { |
| 302 | Some(idx) => idx, |
| 303 | None => return false, |
| 304 | }; |
| 305 | |
| 306 | // Remove from machines |
| 307 | self.machines.remove(machine_idx); |
| 308 | |
| 309 | // Remove from config |
| 310 | self.config.machines.neighbors.remove(config_idx); |
| 311 | |
| 312 | // Update config indices for remaining machines |
| 313 | for machine in &mut self.machines { |
| 314 | if let Some(idx) = machine.config_index { |
| 315 | if idx > config_idx { |
| 316 | machine.config_index = Some(idx - 1); |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | true |
| 322 | } |
| 323 | |
| 324 | /// Save config to file |
| 325 | pub fn save_config(&mut self) -> Result<(), String> { |
| 326 | self.config |
| 327 | .save(&self.config_path) |
| 328 | .map_err(|e| e.to_string()) |
| 329 | } |
| 330 | } |
| 331 |