Add keyboard tab switching
This commit is contained in:
@@ -6,7 +6,10 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use shell::ElyShell;
|
use shell::ElyShell;
|
||||||
|
|
||||||
actions!(ely_app, [CloseCurrentTab, FocusAddressBar, OpenNewTab, Quit]);
|
actions!(
|
||||||
|
ely_app,
|
||||||
|
[CloseCurrentTab, FocusAddressBar, OpenNewTab, Quit, SelectNextTab, SelectPreviousTab,]
|
||||||
|
);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
Application::new().run(|cx: &mut App| {
|
Application::new().run(|cx: &mut App| {
|
||||||
@@ -19,6 +22,10 @@ fn main() {
|
|||||||
KeyBinding::new("ctrl-l", FocusAddressBar, None),
|
KeyBinding::new("ctrl-l", FocusAddressBar, None),
|
||||||
KeyBinding::new("cmd-w", CloseCurrentTab, None),
|
KeyBinding::new("cmd-w", CloseCurrentTab, None),
|
||||||
KeyBinding::new("ctrl-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),
|
KeyBinding::new("cmd-q", Quit, None),
|
||||||
]);
|
]);
|
||||||
cx.set_menus(vec![
|
cx.set_menus(vec![
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use gpui_component::{
|
|||||||
input::{Input, InputEvent, InputState, SelectAll},
|
input::{Input, InputEvent, InputState, SelectAll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{CloseCurrentTab, FocusAddressBar, OpenNewTab};
|
use crate::{CloseCurrentTab, FocusAddressBar, OpenNewTab, SelectNextTab, SelectPreviousTab};
|
||||||
|
|
||||||
enum ShellState {
|
enum ShellState {
|
||||||
Ready(BrowserCore),
|
Ready(BrowserCore),
|
||||||
@@ -94,6 +94,24 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_next_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
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<Self>) {
|
||||||
|
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<Self>) {
|
fn close_active_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let ShellState::Ready(core) = &mut self.state
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
&& core.close_active_tab().is_ok()
|
&& core.close_active_tab().is_ok()
|
||||||
@@ -125,6 +143,24 @@ impl ElyShell {
|
|||||||
self.open_new_tab(window, cx);
|
self.open_new_tab(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_select_next_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &SelectNextTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.select_next_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_select_previous_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &SelectPreviousTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.select_previous_tab(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_address_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn sync_address_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let ShellState::Ready(core) = &mut self.state else {
|
let ShellState::Ready(core) = &mut self.state else {
|
||||||
return;
|
return;
|
||||||
@@ -170,6 +206,8 @@ impl ElyShell {
|
|||||||
.on_action(cx.listener(Self::on_close_current_tab))
|
.on_action(cx.listener(Self::on_close_current_tab))
|
||||||
.on_action(cx.listener(Self::on_focus_address_bar))
|
.on_action(cx.listener(Self::on_focus_address_bar))
|
||||||
.on_action(cx.listener(Self::on_open_new_tab))
|
.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))
|
.bg(rgb(ELY_THEME.canvas))
|
||||||
.text_color(rgb(ELY_THEME.ink))
|
.text_color(rgb(ELY_THEME.ink))
|
||||||
.flex()
|
.flex()
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ impl BrowserCore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_next_tab(&mut self) -> Result<TabId, CoreError> {
|
||||||
|
self.select_tab_by_offset(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous_tab(&mut self) -> Result<TabId, CoreError> {
|
||||||
|
self.select_tab_by_offset(-1)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_command_query(&mut self, query: impl Into<String>) {
|
pub fn set_command_query(&mut self, query: impl Into<String>) {
|
||||||
self.command_query = query.into();
|
self.command_query = query.into();
|
||||||
}
|
}
|
||||||
@@ -175,6 +183,22 @@ impl BrowserCore {
|
|||||||
.ok_or(CoreError::MissingActiveTab)
|
.ok_or(CoreError::MissingActiveTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_tab_by_offset(&mut self, offset: isize) -> Result<TabId, CoreError> {
|
||||||
|
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<usize, CoreError> {
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|tab| tab.id() == &self.active_tab_id)
|
||||||
|
.ok_or(CoreError::MissingActiveTab)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_tab(&self, url: UrlText) -> BrowserTab {
|
fn build_tab(&self, url: UrlText) -> BrowserTab {
|
||||||
let title = tab_title(&url);
|
let title = tab_title(&url);
|
||||||
BrowserTab::new(
|
BrowserTab::new(
|
||||||
@@ -254,4 +278,37 @@ mod tests {
|
|||||||
assert_eq!(replacement_tab.url().as_str(), "ely://new-tab");
|
assert_eq!(replacement_tab.url().as_str(), "ely://new-tab");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selects_next_tab_with_wraparound() -> Result<(), Box<dyn Error>> {
|
||||||
|
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<dyn Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user