Rust · 9434 bytes Raw Blame History
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