diff --git a/crates/ely_app/src/main.rs b/crates/ely_app/src/main.rs index f25b570..6d9af12 100644 --- a/crates/ely_app/src/main.rs +++ b/crates/ely_app/src/main.rs @@ -6,7 +6,10 @@ use gpui::{ }; use shell::ElyShell; -actions!(ely_app, [CloseCurrentTab, FocusAddressBar, OpenNewTab, Quit]); +actions!( + ely_app, + [CloseCurrentTab, FocusAddressBar, OpenNewTab, Quit, SelectNextTab, SelectPreviousTab,] +); fn main() { Application::new().run(|cx: &mut App| { @@ -19,6 +22,10 @@ fn main() { KeyBinding::new("ctrl-l", FocusAddressBar, None), KeyBinding::new("cmd-w", CloseCurrentTab, None), KeyBinding::new("ctrl-w", CloseCurrentTab, None), + KeyBinding::new("cmd-shift-]", SelectNextTab, None), + KeyBinding::new("ctrl-tab", SelectNextTab, None), + KeyBinding::new("cmd-shift-[", SelectPreviousTab, None), + KeyBinding::new("ctrl-shift-tab", SelectPreviousTab, None), KeyBinding::new("cmd-q", Quit, None), ]); cx.set_menus(vec![ diff --git a/crates/ely_app/src/shell.rs b/crates/ely_app/src/shell.rs index 46ef6ef..b6514af 100644 --- a/crates/ely_app/src/shell.rs +++ b/crates/ely_app/src/shell.rs @@ -12,7 +12,7 @@ use gpui_component::{ input::{Input, InputEvent, InputState, SelectAll}, }; -use crate::{CloseCurrentTab, FocusAddressBar, OpenNewTab}; +use crate::{CloseCurrentTab, FocusAddressBar, OpenNewTab, SelectNextTab, SelectPreviousTab}; enum ShellState { Ready(BrowserCore), @@ -94,6 +94,24 @@ impl ElyShell { } } + fn select_next_tab(&mut self, window: &mut Window, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.select_next_tab().is_ok() + { + self.sync_address_input(window, cx); + cx.notify(); + } + } + + fn select_previous_tab(&mut self, window: &mut Window, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.select_previous_tab().is_ok() + { + self.sync_address_input(window, cx); + cx.notify(); + } + } + fn close_active_tab(&mut self, window: &mut Window, cx: &mut Context) { if let ShellState::Ready(core) = &mut self.state && core.close_active_tab().is_ok() @@ -125,6 +143,24 @@ impl ElyShell { self.open_new_tab(window, cx); } + fn on_select_next_tab( + &mut self, + _: &SelectNextTab, + window: &mut Window, + cx: &mut Context, + ) { + self.select_next_tab(window, cx); + } + + fn on_select_previous_tab( + &mut self, + _: &SelectPreviousTab, + window: &mut Window, + cx: &mut Context, + ) { + self.select_previous_tab(window, cx); + } + fn sync_address_input(&mut self, window: &mut Window, cx: &mut Context) { let ShellState::Ready(core) = &mut self.state else { return; @@ -170,6 +206,8 @@ impl ElyShell { .on_action(cx.listener(Self::on_close_current_tab)) .on_action(cx.listener(Self::on_focus_address_bar)) .on_action(cx.listener(Self::on_open_new_tab)) + .on_action(cx.listener(Self::on_select_next_tab)) + .on_action(cx.listener(Self::on_select_previous_tab)) .bg(rgb(ELY_THEME.canvas)) .text_color(rgb(ELY_THEME.ink)) .flex() diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 2916534..f554bbc 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -128,6 +128,14 @@ impl BrowserCore { Ok(()) } + pub fn select_next_tab(&mut self) -> Result { + self.select_tab_by_offset(1) + } + + pub fn select_previous_tab(&mut self) -> Result { + self.select_tab_by_offset(-1) + } + pub fn set_command_query(&mut self, query: impl Into) { self.command_query = query.into(); } @@ -175,6 +183,22 @@ impl BrowserCore { .ok_or(CoreError::MissingActiveTab) } + fn select_tab_by_offset(&mut self, offset: isize) -> Result { + let active_index = self.active_tab_index()?; + let tab_count = self.tabs.len() as isize; + let next_index = (active_index as isize + offset).rem_euclid(tab_count) as usize; + let next_tab_id = self.tabs[next_index].id().clone(); + self.select_tab(&next_tab_id)?; + Ok(next_tab_id) + } + + fn active_tab_index(&self) -> Result { + self.tabs + .iter() + .position(|tab| tab.id() == &self.active_tab_id) + .ok_or(CoreError::MissingActiveTab) + } + fn build_tab(&self, url: UrlText) -> BrowserTab { let title = tab_title(&url); BrowserTab::new( @@ -254,4 +278,37 @@ mod tests { assert_eq!(replacement_tab.url().as_str(), "ely://new-tab"); Ok(()) } + + #[test] + fn selects_next_tab_with_wraparound() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let first_tab_id = core.active_tab()?.id().clone(); + let second_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + let third_tab_id = core.open_tab(UrlText::parse("https://servo.org")?); + + core.select_tab(&first_tab_id)?; + let next_tab_id = core.select_next_tab()?; + assert_eq!(next_tab_id, second_tab_id); + + core.select_tab(&third_tab_id)?; + let wrapped_tab_id = core.select_next_tab()?; + assert_eq!(wrapped_tab_id, first_tab_id); + Ok(()) + } + + #[test] + fn selects_previous_tab_with_wraparound() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let first_tab_id = core.active_tab()?.id().clone(); + let second_tab_id = core.open_tab(UrlText::parse("https://example.com")?); + let third_tab_id = core.open_tab(UrlText::parse("https://servo.org")?); + + let previous_tab_id = core.select_previous_tab()?; + assert_eq!(previous_tab_id, second_tab_id); + + core.select_tab(&first_tab_id)?; + let wrapped_tab_id = core.select_previous_tab()?; + assert_eq!(wrapped_tab_id, third_tab_id); + Ok(()) + } }