diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-19 21:32:40 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-19 23:06:48 +0300 |
| commit | d2074b12f1b2a2a97e188f0241863d6cb858efaf (patch) | |
| tree | 99de2372409ac78a2ccf5cd16fc10ddfbf55174b /src/app/input | |
| parent | aa89f536a28ad11d5931649e7a178863346b6d69 (diff) | |
feat: add hosted product announcements
Diffstat (limited to 'src/app/input')
| -rw-r--r-- | src/app/input/mod.rs | 27 | ||||
| -rw-r--r-- | src/app/input/mouse.rs | 1 | ||||
| -rw-r--r-- | src/app/input/overlays.rs | 165 |
3 files changed, 193 insertions, 0 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 93152e55..18dd8856 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -62,6 +62,7 @@ impl App { match self.state.mode { Mode::Onboarding => self.handle_onboarding_key(key), Mode::ReleaseNotes => self.handle_release_notes_key(key), + Mode::ProductAnnouncement => self.handle_product_announcement_key(key), Mode::Navigate => unreachable!(), Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane => { handle_rename_key(&mut self.state, key) @@ -128,6 +129,32 @@ impl App { } } + pub(crate) fn handle_product_announcement_key(&mut self, key: KeyEvent) { + match key.code { + KeyCode::Up | KeyCode::Char('k') => self.scroll_product_announcement(-1), + KeyCode::Down | KeyCode::Char('j') => self.scroll_product_announcement(1), + KeyCode::PageUp => self.scroll_product_announcement(-8), + KeyCode::PageDown => self.scroll_product_announcement(8), + KeyCode::Home => { + if let Some(announcement) = &mut self.state.product_announcement { + announcement.scroll = 0; + } + } + KeyCode::End => { + let max_scroll = self.state.product_announcement_max_scroll(); + if let Some(announcement) = &mut self.state.product_announcement { + announcement.scroll = max_scroll; + } + } + _ => { + if let Some(ModalAction::Close) = modal_action_from_key(&key, RELEASE_NOTES_ACTIONS) + { + self.dismiss_product_announcement(); + } + } + } + } + pub(super) fn handle_mouse(&mut self, mouse: MouseEvent) { if self.handle_overlay_mouse(mouse) { return; diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index 561aa3c4..e3f13d77 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -518,6 +518,7 @@ impl AppState { self.set_sidebar_section_split(mouse.row); } DragTarget::ReleaseNotesScrollbar { .. } + | DragTarget::ProductAnnouncementScrollbar { .. } | DragTarget::KeybindHelpScrollbar { .. } => {} } } diff --git a/src/app/input/overlays.rs b/src/app/input/overlays.rs index b283847e..365de6b6 100644 --- a/src/app/input/overlays.rs +++ b/src/app/input/overlays.rs @@ -67,6 +67,58 @@ impl App { return true; } + if self.state.mode == Mode::ProductAnnouncement { + match mouse.kind { + MouseEventKind::Down(MouseButton::Left) + if self + .state + .product_announcement_close_button_at(mouse.column, mouse.row) => + { + self.dismiss_product_announcement(); + } + MouseEventKind::Down(MouseButton::Left) => { + if let Some(target) = self + .state + .product_announcement_scrollbar_target_at(mouse.column, mouse.row) + { + match target { + ScrollbarClickTarget::Thumb { grab_row_offset } => { + self.state.drag = Some(DragState { + target: DragTarget::ProductAnnouncementScrollbar { + grab_row_offset, + }, + }); + } + ScrollbarClickTarget::Track { offset_from_bottom } => self + .state + .set_product_announcement_offset_from_bottom(offset_from_bottom), + } + } + } + MouseEventKind::Drag(MouseButton::Left) => { + if let Some(DragState { + target: DragTarget::ProductAnnouncementScrollbar { grab_row_offset }, + }) = &self.state.drag + { + if let Some(offset_from_bottom) = self + .state + .product_announcement_offset_for_drag_row(mouse.row, *grab_row_offset) + { + self.state + .set_product_announcement_offset_from_bottom(offset_from_bottom); + } + } + } + MouseEventKind::Up(MouseButton::Left) => { + self.state.drag = None; + } + MouseEventKind::ScrollUp => self.scroll_product_announcement(-3), + MouseEventKind::ScrollDown => self.scroll_product_announcement(3), + _ => {} + } + return true; + } + if self.state.mode == Mode::KeybindHelp { match mouse.kind { MouseEventKind::Down(MouseButton::Left) @@ -156,6 +208,13 @@ impl AppState { ) } + fn product_announcement_modal_inner(&self) -> Option<Rect> { + self.onboarding_modal_inner( + crate::ui::PRODUCT_ANNOUNCEMENT_MODAL_SIZE.0, + crate::ui::PRODUCT_ANNOUNCEMENT_MODAL_SIZE.1, + ) + } + fn release_notes_close_button_at(&self, col: u16, row: u16) -> bool { let Some(inner) = self.release_notes_modal_inner() else { return false; @@ -267,6 +326,112 @@ impl AppState { } } + fn product_announcement_close_button_at(&self, col: u16, row: u16) -> bool { + let Some(inner) = self.product_announcement_modal_inner() else { + return false; + }; + if inner.height < 4 || inner.width < 12 { + return false; + } + let button = + crate::ui::release_notes_close_button_rect(Rect::new(inner.x, inner.y, inner.width, 1)); + col >= button.x + && col < button.x + button.width + && row >= button.y + && row < button.y + button.height + } + + fn product_announcement_body_rect(&self) -> Option<Rect> { + let inner = self.product_announcement_modal_inner()?; + if inner.height < 8 || inner.width < 4 { + return None; + } + Some(crate::ui::modal_stack_areas(inner, 2, 1, 0, 1).content) + } + + fn product_announcement_scroll_metrics(&self) -> Option<crate::pane::ScrollMetrics> { + let announcement = self.product_announcement.as_ref()?; + let body = self.product_announcement_body_rect()?; + let viewport_rows = body.height.max(1) as usize; + let lines = crate::ui::product_announcement_display_lines(announcement, &self.palette); + + let rows_for_width = |wrap_width: usize| { + lines + .iter() + .map(|(width, _)| width.max(&1).div_ceil(wrap_width.max(1))) + .sum::<usize>() + }; + + let full_width = body.width.max(1) as usize; + let mut total_rows = rows_for_width(full_width); + let wrap_width = if total_rows > viewport_rows && full_width > 1 { + body.width.saturating_sub(1).max(1) as usize + } else { + full_width + }; + total_rows = rows_for_width(wrap_width); + + let max_offset_from_bottom = total_rows.saturating_sub(viewport_rows); + Some(crate::pane::ScrollMetrics { + offset_from_bottom: max_offset_from_bottom.saturating_sub(announcement.scroll as usize), + max_offset_from_bottom, + viewport_rows, + }) + } + + pub(crate) fn product_announcement_max_scroll(&self) -> u16 { + self.product_announcement_scroll_metrics() + .map(|metrics| metrics.max_offset_from_bottom as u16) + .unwrap_or(0) + } + + fn product_announcement_scrollbar_target_at( + &self, + col: u16, + row: u16, + ) -> Option<ScrollbarClickTarget> { + let body = self.product_announcement_body_rect()?; + let metrics = self.product_announcement_scroll_metrics()?; + let track = crate::ui::release_notes_scrollbar_rect(body, metrics)?; + if !(col >= track.x + && col < track.x + track.width + && row >= track.y + && row < track.y + track.height) + { + return None; + } + if let Some(grab_row_offset) = crate::ui::scrollbar_thumb_grab_offset(metrics, track, row) { + Some(ScrollbarClickTarget::Thumb { grab_row_offset }) + } else { + Some(ScrollbarClickTarget::Track { + offset_from_bottom: crate::ui::scrollbar_offset_from_row(metrics, track, row), + }) + } + } + + fn product_announcement_offset_for_drag_row( + &self, + row: u16, + grab_row_offset: u16, + ) -> Option<usize> { + let body = self.product_announcement_body_rect()?; + let metrics = self.product_announcement_scroll_metrics()?; + let track = crate::ui::release_notes_scrollbar_rect(body, metrics)?; + Some(crate::ui::scrollbar_offset_from_drag_row( + metrics, + track, + row, + grab_row_offset, + )) + } + + fn set_product_announcement_offset_from_bottom(&mut self, offset_from_bottom: usize) { + let max_scroll = self.product_announcement_max_scroll() as usize; + if let Some(announcement) = &mut self.product_announcement { + announcement.scroll = max_scroll.saturating_sub(offset_from_bottom) as u16; + } + } + pub(super) fn handle_onboarding_mouse(&mut self, mouse: MouseEvent) { if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { return; |
