1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4impl App {
5 pub fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<Action> {
11 if matches!(mouse.kind, MouseEventKind::Moved) {
16 self.hover_action = self.resolve_region(mouse.column, mouse.row);
17 }
18 let mut action = match self.mode.clone() {
19 Mode::Board => self.handle_board_mouse(mouse),
20 Mode::Detail => self.handle_detail_mouse(mouse),
21 Mode::ConfirmDelete => self.handle_confirm_delete_mouse(mouse),
22 Mode::Help { .. } => self.handle_help_mouse(mouse),
23 Mode::ActionMenu { .. } => self.handle_action_menu_mouse(mouse),
24 Mode::FilterModal => self.handle_filter_modal_mouse(mouse),
25 Mode::Settings => self.handle_settings_mouse(mouse),
26 Mode::Views => self.handle_views_mouse(mouse),
27 Mode::DatePicker { .. } => self.handle_date_picker_mouse(mouse),
28 Mode::Schedule => self.handle_schedule_mouse(mouse),
29 _ => Ok(Action::None),
30 }?;
31 if action == Action::None
32 && matches!(mouse.kind, MouseEventKind::Down(_))
33 && !matches!(
34 self.mode,
35 Mode::ActionMenu { .. }
36 | Mode::ConfirmArchive { .. }
39 | Mode::EditConflict { .. }
44 | Mode::EditRepair { .. }
48 | Mode::Tips { .. }
52 )
53 {
54 if let Some(region_action) = self.resolve_region(mouse.column, mouse.row) {
61 action = region_action;
62 }
63 }
64 Ok(action)
65 }
66
67 pub fn record_region(&self, rect: Rect, action: Action) {
72 self.frame_hit_regions
73 .borrow_mut()
74 .push(HitRegion { rect, action });
75 }
76
77 pub fn is_hovered(&self, action: &Action) -> bool {
81 self.hover_action.as_ref() == Some(action)
82 }
83
84 pub(in crate::app) fn resolve_region(&self, x: u16, y: u16) -> Option<Action> {
86 self.frame_hit_regions
87 .borrow()
88 .iter()
89 .rev()
90 .find(|region| region.rect.contains(Position::new(x, y)))
91 .map(|region| region.action.clone())
92 }
93
94 pub(in crate::app) fn handle_board_mouse(&mut self, mouse: MouseEvent) -> Result<Action> {
101 match mouse.kind {
102 MouseEventKind::Down(MouseButton::Left) => {
103 self.drag_preview_col = None;
104 let hit = self.board_hit_at(mouse.column, mouse.row);
105 self.mouse_down_left(hit)
106 }
107 MouseEventKind::Down(MouseButton::Right) => {
108 self.drag_preview_col = None;
109 let hit = self.board_hit_at(mouse.column, mouse.row);
110 self.open_action_menu(hit, true)
111 }
112 MouseEventKind::Up(MouseButton::Left) => {
113 self.drag_preview_col = None;
114 self.mouse_up_left(mouse.column, mouse.row)
115 }
116 MouseEventKind::Drag(_) => {
117 self.mouse_drag();
118 self.update_drag_preview(mouse.column, mouse.row);
119 Ok(Action::None)
120 }
121 MouseEventKind::ScrollUp => {
122 self.board.move_up();
123 Ok(Action::None)
124 }
125 MouseEventKind::ScrollDown => {
126 self.board.move_down();
127 Ok(Action::None)
128 }
129 MouseEventKind::Moved => {
130 self.drag_preview_col = None;
131 if let Some(tab) = self.tab_hover_at(mouse.column, mouse.row) {
132 self.tabs.hovered = Some(tab);
133 self.board.hover = None;
134 self.board.hover_col = None;
135 } else {
136 self.tabs.hovered = None;
137 self.mouse_hover(mouse.column, mouse.row);
138 }
139 Ok(Action::None)
140 }
141 _ => Ok(Action::None),
142 }
143 }
144
145 pub(in crate::app) fn tab_hover_at(&self, x: u16, y: u16) -> Option<usize> {
148 if !self.tabs.needs_bar() {
149 return None;
150 }
151 let area = self.frame_size.get();
152 if y < area.y || y >= area.y + TAB_HEIGHT {
153 return None;
154 }
155 let tabs_area = Rect {
156 y: area.y,
157 height: TAB_HEIGHT,
158 ..area
159 };
160 (0..self.tabs.len()).find(|&index| {
161 self.tabs
162 .tab_rect(index, tabs_area)
163 .is_some_and(|rect| rect.contains(Position::new(x, y)))
164 })
165 }
166
167 pub(in crate::app) fn mouse_hover(&mut self, x: u16, y: u16) {
173 match self.board_hit_at(x, y) {
174 Some(HitTarget::Card { col, row }) => {
175 self.board.hover = Some((col, row));
176 self.board.hover_col = Some(col);
177 }
178 Some(HitTarget::Column { col }) => {
179 self.board.hover = None;
180 self.board.hover_col = Some(col);
181 }
182 None => {
183 self.board.hover = None;
184 self.board.hover_col = None;
185 }
186 }
187 }
188
189 pub(in crate::app) fn update_drag_preview(&mut self, x: u16, y: u16) {
191 match self.board_hit_at(x, y) {
192 Some(HitTarget::Card { col, row }) => {
193 self.drag_preview_col = Some(col);
194 self.board.hover = Some((col, row));
195 self.board.hover_col = Some(col);
196 }
197 Some(HitTarget::Column { col }) if col < self.board.real_cols() => {
198 self.drag_preview_col = Some(col);
199 self.board.hover = None;
200 self.board.hover_col = Some(col);
201 }
202 _ => {
203 self.drag_preview_col = None;
204 self.board.hover = None;
205 self.board.hover_col = None;
206 }
207 }
208 }
209
210 pub(in crate::app) fn handle_detail_mouse(&mut self, mouse: MouseEvent) -> Result<Action> {
212 match mouse.kind {
213 MouseEventKind::ScrollUp => {
214 if let Some(d) = self.detail.as_mut() {
215 d.scroll_by(-i32::from(WHEEL_ROWS));
216 }
217 }
218 MouseEventKind::ScrollDown => {
219 if let Some(d) = self.detail.as_mut() {
220 d.scroll_by(i32::from(WHEEL_ROWS));
221 }
222 }
223 _ => {}
224 }
225 Ok(Action::None)
226 }
227
228 pub(in crate::app) fn board_mouse_area(&self, area: Rect) -> Option<Rect> {
233 let tab_h: u16 = if self.tabs.needs_bar() { TAB_HEIGHT } else { 0 };
234 let band_h = filter_band_height(self);
238 let hint_h: u16 = if self.one_shot { 0 } else { HINT_HEIGHT };
239 let mention_h = mention_row_height(self);
240 if area.height <= hint_h + mention_h + tab_h + HOVER_HEIGHT + band_h {
241 return None;
242 }
243 Some(Rect {
244 y: area.y + tab_h + HOVER_HEIGHT + band_h,
245 height: area.height - tab_h - HOVER_HEIGHT - band_h - hint_h - mention_h,
246 ..area
247 })
248 }
249
250 pub(in crate::app) fn board_hit_at(&self, x: u16, y: u16) -> Option<HitTarget> {
253 let frame = self.frame_size.get();
254 let board_area = self.board_mouse_area(frame)?;
255 self.board.hit_test(
256 board_area,
257 board::BoardDisplay::from(self.active_board_config()),
258 self.status_msg.is_some(),
259 x,
260 y,
261 )
262 }
263
264 pub(in crate::app) fn select_at(&mut self, hit: HitTarget) {
267 match hit {
268 HitTarget::Card { col, row } => {
269 self.board.active_col = col;
270 self.board.selected = row;
271 }
272 HitTarget::Column { col } => {
273 self.board.active_col = col;
274 self.board.clamp_selection();
275 }
276 }
277 }
278
279 pub(in crate::app) fn mouse_down_left(&mut self, hit: Option<HitTarget>) -> Result<Action> {
280 let Some(target) = hit else {
281 return Ok(Action::None);
282 };
283 self.select_at(target);
284 let (col, card) = match target {
285 HitTarget::Card { col, row } => (col, Some(row)),
286 HitTarget::Column { col } => (col, None),
287 };
288
289 self.mouse.last_down = Some(MouseDown {
294 col,
295 card,
296 at: Instant::now(),
297 });
298 Ok(Action::None)
299 }
300
301 pub(in crate::app) fn open_detail_for(&mut self, hit: HitTarget) -> Result<Action> {
304 if let HitTarget::Card { col, row } = hit {
305 if let Some(card) = self.board.column_cards(col).get(row).cloned() {
306 let issue = self.store.get(IssueSelector::Filename(card.filename))?;
307 self.open_detail_root(issue)?;
308 }
309 }
310 Ok(Action::None)
311 }
312
313 pub(in crate::app) fn mouse_drag(&mut self) {
318 if self.mouse.drag.is_none() {
319 if let Some(down) = &self.mouse.last_down {
320 if down.card.is_some() {
321 self.mouse.drag = Some(Drag {
322 col: down.col,
323 card: down.card,
324 });
325 }
326 }
327 }
328 }
329
330 pub(in crate::app) fn mouse_up_left(&mut self, x: u16, y: u16) -> Result<Action> {
336 self.board.hover = None;
337 self.board.hover_col = None;
338 if let Some(drag) = self.mouse.drag.take() {
339 self.mouse.last_down = None;
340 if drag.card.is_none() {
341 return Ok(Action::None);
342 }
343 let target = match self.board_hit_at(x, y) {
344 Some(target) => target,
345 None => return Ok(Action::None),
346 };
347 match target {
348 HitTarget::Card { col, row } if col == drag.col => {
349 if Some(row) == drag.card {
350 return Ok(Action::None);
351 }
352 return self.reorder_selected_within_column(col, drag.card.unwrap(), row);
353 }
354 HitTarget::Column { col } if col == drag.col => {
355 return self.reorder_selected_within_column(
356 col,
357 drag.card.unwrap(),
358 self.board.column_cards(col).len().saturating_sub(1),
359 );
360 }
361 HitTarget::Card { col, .. } | HitTarget::Column { col }
362 if col < self.board.real_cols() && col != drag.col =>
363 {
364 return self.move_selected_to(col);
365 }
366 _ => return Ok(Action::None),
367 }
368 }
369
370 if let Some(down) = self.mouse.last_down.as_ref()
374 && let Some(HitTarget::Card { col, row }) = self.board_hit_at(x, y)
375 && down.col == col
376 && down.card == Some(row)
377 {
378 return self.open_action_menu(Some(HitTarget::Card { col, row }), false);
379 }
380 Ok(Action::None)
381 }
382
383 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
384 pub(in crate::app) fn handle_confirm_delete_mouse(
385 &mut self,
386 _mouse: MouseEvent,
387 ) -> Result<Action> {
388 Ok(Action::None)
389 }
390
391 #[allow(clippy::unnecessary_wraps)]
392 pub(in crate::app) fn handle_help_mouse(&mut self, mouse: MouseEvent) -> Result<Action> {
393 match mouse.kind {
397 MouseEventKind::ScrollUp => self.scroll_help(-i32::from(WHEEL_ROWS)),
398 MouseEventKind::ScrollDown => self.scroll_help(i32::from(WHEEL_ROWS)),
399 _ => {}
400 }
401 Ok(Action::None)
402 }
403
404 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
405 pub(in crate::app) fn handle_filter_modal_mouse(
406 &mut self,
407 _mouse: MouseEvent,
408 ) -> Result<Action> {
409 Ok(Action::None)
410 }
411
412 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
413 pub(in crate::app) fn handle_settings_mouse(&mut self, _mouse: MouseEvent) -> Result<Action> {
414 Ok(Action::None)
415 }
416
417 #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
418 pub(in crate::app) fn handle_date_picker_mouse(
419 &mut self,
420 _mouse: MouseEvent,
421 ) -> Result<Action> {
422 Ok(Action::None)
423 }
424}