1use crate::event::{Event, KeyEvent, MouseButton, MouseEvent, MouseKind};
7use crate::geometry::Rect;
8use crate::keymap::Key;
9use crate::style::CellStyle;
10use crate::widget::{EventCx, PaintCx, Widget};
11
12use super::IndexMessage;
13use super::context_item::{self, ContextItem};
14use super::context_menu::{self, ContextMenu};
15use super::edge_scroll::{Edge, EdgeScroll, Zone};
16
17type MoveMessage<Msg> = Box<dyn Fn(usize, usize) -> Msg>;
19
20type MenuItems<Msg> = Box<dyn Fn(usize) -> Vec<ContextItem<Msg>>>;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum TabEdit {
28 Close(usize),
30 Move {
32 from: usize,
34 to: usize,
36 },
37}
38
39impl TabEdit {
40 pub fn apply<T>(self, tabs: &mut Vec<T>, active: &mut usize) {
44 match self {
45 Self::Close(index) => {
46 if index >= tabs.len() {
47 return;
48 }
49 tabs.remove(index);
50 if index < *active {
51 *active -= 1;
52 }
53 *active = (*active).min(tabs.len().saturating_sub(1));
54 }
55 Self::Move { from, to } => {
56 if from >= tabs.len() || to >= tabs.len() || from == to {
57 return;
58 }
59 let tab = tabs.remove(from);
60 tabs.insert(to, tab);
61 *active = if *active == from {
62 to
63 } else if from < *active && to >= *active {
64 *active - 1
65 } else if from > *active && to <= *active {
66 *active + 1
67 } else {
68 *active
69 };
70 }
71 }
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(crate) enum Direction {
78 Across,
80 Down,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub(crate) enum TabHit {
87 Tab(usize, Rect),
89 Close(usize),
91}
92
93#[derive(Debug, Clone, Copy)]
95struct Pressed {
96 index: usize,
97 start: (i32, i32),
98 pointer: (i32, i32),
99 grab: (i32, i32),
100 dragging: bool,
101 close: bool,
102}
103
104#[derive(Debug, Default)]
106struct TabPointer {
107 pressed: Option<Pressed>,
108 edge: EdgeScroll,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub(crate) struct Drag {
115 pub(crate) index: usize,
117 pub(crate) pointer: (i32, i32),
119 pub(crate) grab: (i32, i32),
121}
122
123pub(crate) struct TabModel<Msg> {
125 pub(crate) count: usize,
126 pub(crate) active: usize,
127 pub(crate) pinned: Vec<usize>,
128 pub(crate) on_select: Option<IndexMessage<Msg>>,
129 pub(crate) on_close: Option<IndexMessage<Msg>>,
130 pub(crate) on_move: Option<MoveMessage<Msg>>,
131 pub(crate) on_drag_scroll: Option<IndexMessage<Msg>>,
133 menu: Option<MenuItems<Msg>>,
134}
135
136#[derive(Debug, Default)]
138struct MenuTab(usize);
139
140impl<Msg> TabModel<Msg> {
141 pub(crate) fn new(count: usize) -> Self {
142 Self {
143 count,
144 active: 0,
145 pinned: Vec::new(),
146 on_select: None,
147 on_close: None,
148 on_move: None,
149 on_drag_scroll: None,
150 menu: None,
151 }
152 }
153
154 pub(crate) fn set_context_menu(&mut self, items: impl Fn(usize) -> Vec<ContextItem<Msg>> + 'static) {
155 self.menu = Some(Box::new(items));
156 }
157
158 fn menu_for(&self, index: usize) -> Option<(ContextMenu<usize>, Vec<Msg>)> {
160 let items = self.menu.as_ref()?;
161 let (items, messages) = context_item::keyed(items(index));
162 Some((ContextMenu::new(items), messages))
163 }
164
165 pub(crate) fn has_menu(&self) -> bool {
167 self.menu.is_some()
168 }
169
170 fn open_menu(&self, cx: &mut EventCx<'_, Msg>, index: usize, anchor: Rect, keyboard: bool) {
173 let Some((menu, _)) = self.menu_for(index) else {
174 return;
175 };
176 cx.memory::<MenuTab>().0 = index;
177 cx.with_messages(|cx: &mut EventCx<'_, usize>| menu.open(cx, anchor, keyboard));
178 }
179
180 pub(crate) fn menu_event(
188 &self,
189 cx: &mut EventCx<'_, Msg>,
190 event: &Event,
191 hit: Option<TabHit>,
192 open_tab: Option<Rect>,
193 ) -> bool {
194 if self.menu.is_none() || self.count == 0 {
195 return false;
196 }
197 let right_press = match event {
198 Event::Mouse(mouse) if mouse.kind == MouseKind::Down(MouseButton::Right) => Some((mouse.x, mouse.y)),
199 _ => None,
200 };
201 if context_menu::is_open_in(cx) {
202 let tab = cx.memory::<MenuTab>().0;
203 let elsewhere = right_press.is_some_and(|(x, y)| !context_menu::contains(cx, x, y));
204 if elsewhere || tab >= self.count {
207 cx.with_messages(|cx: &mut EventCx<'_, usize>| ContextMenu::<usize>::close(cx));
208 } else if let Some((menu, messages)) = self.menu_for(tab) {
209 let (used, chosen) = cx.with_messages(|cx| menu.event(cx, event));
210 if let Some(message) = chosen.last().and_then(|chosen| messages.into_iter().nth(*chosen)) {
211 cx.emit(message);
212 }
213 return used;
214 }
215 }
216 match event {
217 Event::Mouse(_) => {
218 let Some((x, y)) = right_press else {
219 return false;
220 };
221 if let Some(TabHit::Tab(index, _) | TabHit::Close(index)) = hit {
222 self.open_menu(cx, index, Rect::new(x, y, 1, 1), false);
223 }
224 true
225 }
226 Event::Key(key) if context_menu::is_menu_key(key) => {
227 let area = cx.area();
228 let anchor = open_tab.unwrap_or(Rect::new(area.x, area.y, 1, 1));
229 self.open_menu(cx, self.active(), anchor, true);
230 true
231 }
232 _ => false,
233 }
234 }
235
236 pub(crate) fn paint_menu(&self, cx: &mut PaintCx<'_>, anchor: Rect) -> bool {
238 match self.menu_tab(cx).and_then(|tab| self.menu_for(tab)) {
239 Some((menu, _)) => {
240 menu.paint_overlay(cx, anchor);
241 true
242 }
243 None => false,
244 }
245 }
246
247 pub(crate) fn menu_tab(&self, cx: &mut PaintCx<'_>) -> Option<usize> {
250 if self.menu.is_none() || !context_menu::is_open(cx) {
251 return None;
252 }
253 Some(cx.memory::<MenuTab>().0).filter(|tab| *tab < self.count)
254 }
255
256 pub(crate) fn set_on_close(&mut self, message: impl Fn(usize) -> Msg + 'static) {
257 self.on_close = Some(Box::new(message));
258 }
259
260 pub(crate) fn set_on_move(&mut self, message: impl Fn(usize, usize) -> Msg + 'static) {
261 self.on_move = Some(Box::new(message));
262 }
263
264 pub(crate) fn active(&self) -> usize {
266 self.active.min(self.count.saturating_sub(1))
267 }
268
269 pub(crate) fn closable(&self, index: usize) -> bool {
271 self.on_close.is_some() && !self.pinned.contains(&index)
272 }
273
274 pub(crate) fn reorderable(&self) -> bool {
275 self.on_move.is_some()
276 }
277
278 pub(crate) fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
280 if index >= self.count {
281 return false;
282 }
283 if index != self.active
284 && let Some(message) = &self.on_select
285 {
286 cx.emit(message(index));
287 }
288 true
289 }
290
291 pub(crate) fn close(&self, cx: &mut EventCx<'_, Msg>, index: usize) -> bool {
293 if index >= self.count || !self.closable(index) {
294 return false;
295 }
296 if let Some(message) = &self.on_close {
297 cx.emit(message(index));
298 }
299 true
300 }
301
302 fn move_tab(&self, cx: &mut EventCx<'_, Msg>, from: usize, to: usize) {
303 if from != to
304 && to < self.count
305 && let Some(message) = &self.on_move
306 {
307 cx.emit(message(from, to));
308 }
309 }
310
311 pub(crate) fn key(&self, cx: &mut EventCx<'_, Msg>, key: &KeyEvent, direction: Direction) -> bool {
315 let (back, forward, back_letter, forward_letter) = match direction {
316 Direction::Across => (Key::Left, Key::Right, 'h', 'l'),
317 Direction::Down => (Key::Up, Key::Down, 'k', 'j'),
318 };
319 let active = self.active();
320 if key.is_plain(back) || key.is_plain(Key::Char(back_letter)) {
321 return active > 0 && self.open(cx, active - 1);
322 }
323 if key.is_plain(forward) || key.is_plain(Key::Char(forward_letter)) {
324 return self.open(cx, active + 1);
325 }
326 let mods = key.chord.mods;
327 if self.on_close.is_some() && mods.ctrl && !mods.shift && !mods.alt && key.chord.key == Key::Char('w') {
328 self.close(cx, active);
329 return true;
330 }
331 if self.reorderable() && mods.ctrl && mods.shift && !mods.alt {
332 if key.chord.key == back {
333 if active > 0 {
334 self.move_tab(cx, active, active - 1);
335 }
336 return true;
337 }
338 if key.chord.key == forward {
339 self.move_tab(cx, active, active + 1);
340 return true;
341 }
342 }
343 false
344 }
345
346 pub(crate) fn pointer(
349 &self,
350 cx: &mut EventCx<'_, Msg>,
351 mouse: &MouseEvent,
352 hit: Option<TabHit>,
353 slots: &[(usize, Rect)],
354 direction: Direction,
355 ) -> bool {
356 let at = (mouse.x, mouse.y);
357 match mouse.kind {
358 MouseKind::Down(MouseButton::Middle) => match hit {
359 Some(TabHit::Tab(index, _) | TabHit::Close(index)) if self.on_close.is_some() => {
360 self.close(cx, index);
361 true
362 }
363 _ => false,
364 },
365 MouseKind::Down(MouseButton::Left) => match hit {
366 Some(TabHit::Close(index)) => {
367 cx.capture_pointer();
368 let press = Pressed { index, start: at, pointer: at, grab: (0, 0), dragging: false, close: true };
369 *cx.memory::<TabPointer>() = TabPointer { pressed: Some(press), edge: EdgeScroll::default() };
370 true
371 }
372 Some(TabHit::Tab(index, rect)) => {
373 self.open(cx, index);
374 if self.reorderable() {
375 cx.capture_pointer();
376 let grab = (at.0 - rect.x, at.1 - rect.y);
377 let press = Pressed { index, start: at, pointer: at, grab, dragging: false, close: false };
378 *cx.memory::<TabPointer>() = TabPointer { pressed: Some(press), edge: EdgeScroll::default() };
379 }
380 true
381 }
382 None => false,
383 },
384 MouseKind::Drag(MouseButton::Left) => {
385 let memory = cx.memory::<TabPointer>();
386 let Some(press) = &mut memory.pressed else {
387 return false;
388 };
389 press.pointer = at;
390 let travel = match direction {
391 Direction::Across => (at.0 - press.start.0).abs() >= 2,
392 Direction::Down => (at.1 - press.start.1).abs() >= 1,
393 };
394 if !press.close && travel {
395 press.dragging = true;
396 }
397 true
398 }
399 MouseKind::Up(MouseButton::Left) => {
400 let memory = cx.memory::<TabPointer>();
401 memory.edge = EdgeScroll::default();
402 let Some(press) = memory.pressed.take() else {
403 return false;
404 };
405 if press.close {
406 if hit == Some(TabHit::Close(press.index)) {
407 self.close(cx, press.index);
408 }
409 } else if press.dragging {
410 let to = drop_target(slots, press.index, at, direction);
411 self.move_tab(cx, press.index, to);
412 }
413 true
414 }
415 _ => false,
416 }
417 }
418
419 pub(crate) fn edge_scroll(
425 &self,
426 cx: &mut EventCx<'_, Msg>,
427 zone: Option<Zone>,
428 step: impl FnOnce(&mut EventCx<'_, Msg>, Edge) -> Option<usize>,
429 ) {
430 let memory = cx.memory::<TabPointer>();
431 let dragging = memory.pressed.is_some_and(|press| press.dragging);
432 let mut edge = memory.edge;
433 let first = edge.drive(cx, zone.filter(|_| dragging), step);
434 cx.memory::<TabPointer>().edge = edge;
435 if let (Some(first), Some(message)) = (first, &self.on_drag_scroll) {
436 cx.emit(message(first));
437 }
438 }
439
440 pub(crate) fn drag(&self, cx: &mut PaintCx<'_>) -> Option<Drag> {
442 let press = cx.memory::<TabPointer>().pressed?;
443 (press.dragging && press.index < self.count).then_some(Drag {
444 index: press.index,
445 pointer: press.pointer,
446 grab: press.grab,
447 })
448 }
449}
450
451pub(crate) fn paint_drop_slot(cx: &mut PaintCx<'_>, rect: Rect) {
453 let drop = cx.style("tab-drop", None, &[]).text();
454 cx.clear(rect, drop.bg.unwrap_or_else(|| cx.color("raised")));
455}
456
457pub(crate) fn paint_ghost_surface(cx: &mut PaintCx<'_>, rect: Rect) -> CellStyle {
460 let ghost = cx.style("tab-ghost", None, &[]).text();
461 cx.clear(rect, ghost.bg.unwrap_or_else(|| cx.color("active")));
462 ghost
463}
464
465pub(crate) fn drop_target(slots: &[(usize, Rect)], from: usize, pointer: (i32, i32), direction: Direction) -> usize {
469 let along = |rect: &Rect| match direction {
470 Direction::Across => rect.x,
471 Direction::Down => rect.y,
472 };
473 let position = match direction {
474 Direction::Across => pointer.0,
475 Direction::Down => pointer.1,
476 };
477 let Some((first, _)) = slots.first() else {
478 return from;
479 };
480 slots.iter().take_while(|(_, rect)| along(rect) <= position).last().map_or(*first, |(index, _)| *index)
481}
482
483pub(crate) fn preview_order(count: usize, drag: Option<(usize, usize)>) -> Vec<usize> {
485 let mut order: Vec<usize> = (0..count).collect();
486 if let Some((from, to)) = drag
487 && from < count
488 && to < count
489 {
490 let tab = order.remove(from);
491 order.insert(to, tab);
492 }
493 order
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn edits_keep_the_open_tab() {
502 let mut tabs = vec!["a", "b", "c", "d"];
503 let mut active = 2;
504 TabEdit::Close(0).apply(&mut tabs, &mut active);
505 assert_eq!((tabs.as_slice(), active), (&["b", "c", "d"][..], 1));
506 TabEdit::Close(1).apply(&mut tabs, &mut active);
507 assert_eq!((tabs.as_slice(), active), (&["b", "d"][..], 1));
508 TabEdit::Close(1).apply(&mut tabs, &mut active);
509 assert_eq!((tabs.as_slice(), active), (&["b"][..], 0));
510
511 let mut tabs = vec!["a", "b", "c", "d"];
512 let mut active = 1;
513 TabEdit::Move { from: 1, to: 3 }.apply(&mut tabs, &mut active);
514 assert_eq!((tabs.as_slice(), active), (&["a", "c", "d", "b"][..], 3));
515 TabEdit::Move { from: 0, to: 3 }.apply(&mut tabs, &mut active);
516 assert_eq!((tabs.as_slice(), active), (&["c", "d", "b", "a"][..], 2));
517 TabEdit::Move { from: 3, to: 0 }.apply(&mut tabs, &mut active);
518 assert_eq!((tabs.as_slice(), active), (&["a", "c", "d", "b"][..], 3));
519 TabEdit::Move { from: 9, to: 0 }.apply(&mut tabs, &mut active);
520 assert_eq!(active, 3);
521 }
522
523 #[test]
524 fn drop_target_is_the_resting_tab_under_the_pointer() {
525 let slots = [(2, Rect::new(4, 0, 6, 1)), (3, Rect::new(11, 0, 9, 1)), (4, Rect::new(21, 0, 5, 1))];
526 assert_eq!(drop_target(&slots, 3, (0, 0), Direction::Across), 2);
527 assert_eq!(drop_target(&slots, 2, (12, 0), Direction::Across), 3);
528 assert_eq!(drop_target(&slots, 2, (40, 0), Direction::Across), 4);
529 assert_eq!(preview_order(4, Some((0, 2))), vec![1, 2, 0, 3]);
530 }
531}