Close current browser tab
This commit is contained in:
+20
-10
@@ -6,21 +6,31 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use shell::ElyShell;
|
use shell::ElyShell;
|
||||||
|
|
||||||
actions!(ely_app, [Quit]);
|
actions!(ely_app, [CloseCurrentTab, Quit]);
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
Application::new().run(|cx: &mut App| {
|
Application::new().run(|cx: &mut App| {
|
||||||
gpui_component::init(cx);
|
gpui_component::init(cx);
|
||||||
cx.on_action(quit);
|
cx.on_action(quit);
|
||||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
cx.bind_keys([
|
||||||
cx.set_menus(vec![Menu {
|
KeyBinding::new("cmd-w", CloseCurrentTab, None),
|
||||||
name: "ELY Browser".into(),
|
KeyBinding::new("ctrl-w", CloseCurrentTab, None),
|
||||||
items: vec![
|
KeyBinding::new("cmd-q", Quit, None),
|
||||||
MenuItem::os_submenu("Services", SystemMenuType::Services),
|
]);
|
||||||
MenuItem::separator(),
|
cx.set_menus(vec![
|
||||||
MenuItem::action("Quit ELY Browser", Quit),
|
Menu {
|
||||||
],
|
name: "ELY Browser".into(),
|
||||||
}]);
|
items: vec![
|
||||||
|
MenuItem::os_submenu("Services", SystemMenuType::Services),
|
||||||
|
MenuItem::separator(),
|
||||||
|
MenuItem::action("Quit ELY Browser", Quit),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Menu {
|
||||||
|
name: "File".into(),
|
||||||
|
items: vec![MenuItem::action("Close Tab", CloseCurrentTab)],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
let bounds = Bounds::centered(None, size(px(1240.0), px(780.0)), cx);
|
let bounds = Bounds::centered(None, size(px(1240.0), px(780.0)), cx);
|
||||||
let opened = cx.open_window(
|
let opened = cx.open_window(
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ use gpui_component::{
|
|||||||
input::{Input, InputEvent, InputState},
|
input::{Input, InputEvent, InputState},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::CloseCurrentTab;
|
||||||
|
|
||||||
enum ShellState {
|
enum ShellState {
|
||||||
Ready(BrowserCore),
|
Ready(BrowserCore),
|
||||||
StartupError(String),
|
StartupError(String),
|
||||||
@@ -81,6 +83,24 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn close_active_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.close_active_tab().is_ok()
|
||||||
|
{
|
||||||
|
self.sync_address_input(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_close_current_tab(
|
||||||
|
&mut self,
|
||||||
|
_: &CloseCurrentTab,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.close_active_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;
|
||||||
@@ -116,6 +136,7 @@ impl ElyShell {
|
|||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.on_action(cx.listener(Self::on_close_current_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()
|
||||||
|
|||||||
@@ -42,18 +42,20 @@ pub struct BrowserCore {
|
|||||||
active_profile_id: ProfileId,
|
active_profile_id: ProfileId,
|
||||||
active_tab_id: TabId,
|
active_tab_id: TabId,
|
||||||
command_query: String,
|
command_query: String,
|
||||||
|
new_tab_url: UrlText,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserCore {
|
impl BrowserCore {
|
||||||
pub fn new(config: InitialBrowserConfig) -> Result<Self, CoreError> {
|
pub fn new(config: InitialBrowserConfig) -> Result<Self, CoreError> {
|
||||||
let space = Space::new(config.space_name, config.space_icon, 0xf54e00);
|
let space = Space::new(config.space_name, config.space_icon, 0xf54e00);
|
||||||
let profile = Profile::new(config.profile_name, 0x26251e, ProfileKind::Standard);
|
let profile = Profile::new(config.profile_name, 0x26251e, ProfileKind::Standard);
|
||||||
|
let new_tab_url = config.initial_url;
|
||||||
let tab = BrowserTab::new(
|
let tab = BrowserTab::new(
|
||||||
TabId::new(),
|
TabId::new(),
|
||||||
space.id().clone(),
|
space.id().clone(),
|
||||||
profile.id().clone(),
|
profile.id().clone(),
|
||||||
"New Tab",
|
"New Tab",
|
||||||
config.initial_url,
|
new_tab_url.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -64,18 +66,12 @@ impl BrowserCore {
|
|||||||
profiles: vec![profile],
|
profiles: vec![profile],
|
||||||
tabs: vec![tab],
|
tabs: vec![tab],
|
||||||
command_query: String::new(),
|
command_query: String::new(),
|
||||||
|
new_tab_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_tab(&mut self, url: UrlText) -> TabId {
|
pub fn open_tab(&mut self, url: UrlText) -> TabId {
|
||||||
let title = tab_title(&url);
|
let tab = self.build_tab(url);
|
||||||
let tab = BrowserTab::new(
|
|
||||||
TabId::new(),
|
|
||||||
self.active_space_id.clone(),
|
|
||||||
self.active_profile_id.clone(),
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
);
|
|
||||||
let tab_id = tab.id().clone();
|
let tab_id = tab.id().clone();
|
||||||
let insert_index = self
|
let insert_index = self
|
||||||
.tabs
|
.tabs
|
||||||
@@ -87,6 +83,38 @@ impl BrowserCore {
|
|||||||
tab_id
|
tab_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn close_active_tab(&mut self) -> Result<TabId, CoreError> {
|
||||||
|
let tab_id = self.active_tab_id.clone();
|
||||||
|
self.close_tab(&tab_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_tab(&mut self, tab_id: &TabId) -> Result<TabId, CoreError> {
|
||||||
|
let close_index = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.position(|tab| tab.id() == tab_id)
|
||||||
|
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
|
||||||
|
let was_active = &self.active_tab_id == tab_id;
|
||||||
|
|
||||||
|
self.tabs.remove(close_index);
|
||||||
|
|
||||||
|
if self.tabs.is_empty() {
|
||||||
|
let tab = self.build_tab(self.new_tab_url.clone());
|
||||||
|
let replacement_id = tab.id().clone();
|
||||||
|
self.tabs.push(tab);
|
||||||
|
self.active_tab_id = replacement_id.clone();
|
||||||
|
return Ok(replacement_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if was_active {
|
||||||
|
let next_index = close_index.min(self.tabs.len() - 1);
|
||||||
|
let next_tab_id = self.tabs[next_index].id().clone();
|
||||||
|
self.select_tab(&next_tab_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.active_tab_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
|
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
|
||||||
let tab = self
|
let tab = self
|
||||||
.tabs
|
.tabs
|
||||||
@@ -146,6 +174,17 @@ impl BrowserCore {
|
|||||||
.find(|tab| tab.id() == &self.active_tab_id)
|
.find(|tab| tab.id() == &self.active_tab_id)
|
||||||
.ok_or(CoreError::MissingActiveTab)
|
.ok_or(CoreError::MissingActiveTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_tab(&self, url: UrlText) -> BrowserTab {
|
||||||
|
let title = tab_title(&url);
|
||||||
|
BrowserTab::new(
|
||||||
|
TabId::new(),
|
||||||
|
self.active_space_id.clone(),
|
||||||
|
self.active_profile_id.clone(),
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_title(url: &UrlText) -> String {
|
fn tab_title(url: &UrlText) -> String {
|
||||||
@@ -163,6 +202,7 @@ mod tests {
|
|||||||
use ely_domain::UrlText;
|
use ely_domain::UrlText;
|
||||||
|
|
||||||
use super::{BrowserCore, InitialBrowserConfig};
|
use super::{BrowserCore, InitialBrowserConfig};
|
||||||
|
use crate::CoreError;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opens_new_tab_below_active_tab() -> Result<(), Box<dyn Error>> {
|
fn opens_new_tab_below_active_tab() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -180,4 +220,38 @@ mod tests {
|
|||||||
assert_eq!(snapshot.active_tab_id, third_tab_id);
|
assert_eq!(snapshot.active_tab_id, third_tab_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closes_active_tab_and_selects_next_neighbor() -> 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(&second_tab_id)?;
|
||||||
|
let active_tab_id = core.close_active_tab()?;
|
||||||
|
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
let ordered_ids = snapshot.tabs.iter().map(|tab| tab.id().clone()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(active_tab_id, third_tab_id);
|
||||||
|
assert_eq!(ordered_ids, vec![first_tab_id, active_tab_id.clone()]);
|
||||||
|
assert_eq!(snapshot.active_tab_id, active_tab_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closing_last_tab_replaces_it_with_new_tab() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let closed_tab_id = core.active_tab()?.id().clone();
|
||||||
|
|
||||||
|
let active_tab_id = core.close_tab(&closed_tab_id)?;
|
||||||
|
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
assert_eq!(snapshot.tabs.len(), 1);
|
||||||
|
assert_eq!(snapshot.active_tab_id, active_tab_id);
|
||||||
|
let replacement_tab = snapshot.tabs.first().ok_or(CoreError::MissingActiveTab)?;
|
||||||
|
assert_eq!(replacement_tab.url().as_str(), "ely://new-tab");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user