@@ -41,6 +41,14 @@ pub struct TabBar { |
| 41 | 41 | hovered_close: Option<usize>, |
| 42 | 42 | /// Cached tab bounds. |
| 43 | 43 | tab_bounds: Vec<Rect>, |
| 44 | + /// Tab being dragged (index). |
| 45 | + dragging_tab: Option<usize>, |
| 46 | + /// Drag start position. |
| 47 | + drag_start: Option<Point>, |
| 48 | + /// Whether drag is active (past threshold). |
| 49 | + drag_active: bool, |
| 50 | + /// Target drop position for reorder. |
| 51 | + drop_target: Option<usize>, |
| 44 | 52 | } |
| 45 | 53 | |
| 46 | 54 | impl TabBar { |
@@ -53,6 +61,10 @@ impl TabBar { |
| 53 | 61 | hovered_tab: None, |
| 54 | 62 | hovered_close: None, |
| 55 | 63 | tab_bounds: Vec::new(), |
| 64 | + dragging_tab: None, |
| 65 | + drag_start: None, |
| 66 | + drag_active: false, |
| 67 | + drop_target: None, |
| 56 | 68 | } |
| 57 | 69 | } |
| 58 | 70 | |
@@ -164,6 +176,107 @@ impl TabBar { |
| 164 | 176 | self.hovered_close = None; |
| 165 | 177 | } |
| 166 | 178 | |
| 179 | + /// Start potential tab drag. |
| 180 | + pub fn start_drag(&mut self, pos: Point) -> bool { |
| 181 | + if !self.bounds.contains_point(pos) { |
| 182 | + return false; |
| 183 | + } |
| 184 | + |
| 185 | + for (i, tab_bounds) in self.tab_bounds.iter().enumerate() { |
| 186 | + if tab_bounds.contains_point(pos) { |
| 187 | + // Don't drag if clicking close button |
| 188 | + if let Some(close_bounds) = self.close_button_bounds(i) { |
| 189 | + if close_bounds.contains_point(pos) { |
| 190 | + return false; |
| 191 | + } |
| 192 | + } |
| 193 | + self.dragging_tab = Some(i); |
| 194 | + self.drag_start = Some(pos); |
| 195 | + self.drag_active = false; |
| 196 | + self.drop_target = None; |
| 197 | + return true; |
| 198 | + } |
| 199 | + } |
| 200 | + false |
| 201 | + } |
| 202 | + |
| 203 | + /// Update tab drag with current mouse position. |
| 204 | + /// Returns true if drag is active. |
| 205 | + pub fn update_drag(&mut self, pos: Point) -> bool { |
| 206 | + if self.dragging_tab.is_none() { |
| 207 | + return false; |
| 208 | + } |
| 209 | + |
| 210 | + // Check if past drag threshold |
| 211 | + if !self.drag_active { |
| 212 | + if let Some(start) = self.drag_start { |
| 213 | + let dx = (pos.x - start.x).abs(); |
| 214 | + if dx > 5 { |
| 215 | + self.drag_active = true; |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + if !self.drag_active { |
| 221 | + return false; |
| 222 | + } |
| 223 | + |
| 224 | + // Calculate drop target based on position |
| 225 | + if !self.bounds.contains_point(pos) { |
| 226 | + self.drop_target = None; |
| 227 | + } else { |
| 228 | + // Find which slot we're closest to |
| 229 | + let mut target = 0; |
| 230 | + for (i, tab_bounds) in self.tab_bounds.iter().enumerate() { |
| 231 | + let mid_x = tab_bounds.x + tab_bounds.width as i32 / 2; |
| 232 | + if pos.x > mid_x { |
| 233 | + target = i + 1; |
| 234 | + } |
| 235 | + } |
| 236 | + self.drop_target = Some(target); |
| 237 | + } |
| 238 | + |
| 239 | + true |
| 240 | + } |
| 241 | + |
| 242 | + /// Complete tab drag and return reorder info if any (from, to). |
| 243 | + pub fn complete_drag(&mut self) -> Option<(usize, usize)> { |
| 244 | + let result = if self.drag_active { |
| 245 | + if let (Some(from), Some(to)) = (self.dragging_tab, self.drop_target) { |
| 246 | + if from != to && to != from + 1 { |
| 247 | + Some((from, to)) |
| 248 | + } else { |
| 249 | + None |
| 250 | + } |
| 251 | + } else { |
| 252 | + None |
| 253 | + } |
| 254 | + } else { |
| 255 | + None |
| 256 | + }; |
| 257 | + |
| 258 | + self.cancel_drag(); |
| 259 | + result |
| 260 | + } |
| 261 | + |
| 262 | + /// Cancel tab drag. |
| 263 | + pub fn cancel_drag(&mut self) { |
| 264 | + self.dragging_tab = None; |
| 265 | + self.drag_start = None; |
| 266 | + self.drag_active = false; |
| 267 | + self.drop_target = None; |
| 268 | + } |
| 269 | + |
| 270 | + /// Check if currently dragging. |
| 271 | + pub fn is_dragging(&self) -> bool { |
| 272 | + self.drag_active |
| 273 | + } |
| 274 | + |
| 275 | + /// Get dragging tab index. |
| 276 | + pub fn dragging_tab(&self) -> Option<usize> { |
| 277 | + self.dragging_tab |
| 278 | + } |
| 279 | + |
| 167 | 280 | /// Render the tab bar. |
| 168 | 281 | pub fn render(&self, renderer: &Renderer) -> anyhow::Result<()> { |
| 169 | 282 | let theme = renderer.theme(); |
@@ -185,6 +298,26 @@ impl TabBar { |
| 185 | 298 | for (i, (tab, bounds)) in self.tabs.iter().zip(self.tab_bounds.iter()).enumerate() { |
| 186 | 299 | let is_active = i == self.active_index; |
| 187 | 300 | let is_hovered = self.hovered_tab == Some(i); |
| 301 | + let is_being_dragged = self.drag_active && self.dragging_tab == Some(i); |
| 302 | + |
| 303 | + // Dim the tab being dragged |
| 304 | + if is_being_dragged { |
| 305 | + renderer.fill_rect(*bounds, theme.item_background.with_alpha(0.3))?; |
| 306 | + continue; // Skip rest of rendering for dragged tab |
| 307 | + } |
| 308 | + |
| 309 | + // Draw drop indicator before this tab if needed |
| 310 | + if self.drag_active && self.drop_target == Some(i) { |
| 311 | + let indicator_x = bounds.x - 2; |
| 312 | + renderer.line( |
| 313 | + indicator_x as f64, |
| 314 | + (bounds.y + 4) as f64, |
| 315 | + indicator_x as f64, |
| 316 | + (bounds.y + bounds.height as i32 - 4) as f64, |
| 317 | + theme.selection_background, |
| 318 | + 3.0, |
| 319 | + )?; |
| 320 | + } |
| 188 | 321 | |
| 189 | 322 | // Tab background |
| 190 | 323 | if is_active { |
@@ -286,6 +419,21 @@ impl TabBar { |
| 286 | 419 | } |
| 287 | 420 | } |
| 288 | 421 | |
| 422 | + // Draw drop indicator at end if dropping after last tab |
| 423 | + if self.drag_active && self.drop_target == Some(self.tabs.len()) { |
| 424 | + if let Some(last_bounds) = self.tab_bounds.last() { |
| 425 | + let indicator_x = last_bounds.x + last_bounds.width as i32 + 2; |
| 426 | + renderer.line( |
| 427 | + indicator_x as f64, |
| 428 | + (last_bounds.y + 4) as f64, |
| 429 | + indicator_x as f64, |
| 430 | + (last_bounds.y + last_bounds.height as i32 - 4) as f64, |
| 431 | + theme.selection_background, |
| 432 | + 3.0, |
| 433 | + )?; |
| 434 | + } |
| 435 | + } |
| 436 | + |
| 289 | 437 | Ok(()) |
| 290 | 438 | } |
| 291 | 439 | } |