Add split view panes

This commit is contained in:
2026-05-08 01:47:19 -04:00
parent ccb3972b75
commit 23b108c8dd
13 changed files with 419 additions and 6 deletions
+4
View File
@@ -23,6 +23,7 @@ actions!(
RestoreClosedTab,
SelectNextTab,
SelectPreviousTab,
SplitRight,
ToggleFavoriteTab,
TogglePinnedTab,
]
@@ -35,6 +36,8 @@ fn main() {
cx.bind_keys([
KeyBinding::new("cmd-t", OpenNewTab, None),
KeyBinding::new("ctrl-t", OpenNewTab, None),
KeyBinding::new("cmd-\\", SplitRight, None),
KeyBinding::new("ctrl-\\", SplitRight, None),
KeyBinding::new("cmd-shift-j", OpenDownloads, None),
KeyBinding::new("ctrl-shift-j", OpenDownloads, None),
KeyBinding::new("cmd-y", OpenHistory, None),
@@ -72,6 +75,7 @@ fn main() {
name: "File".into(),
items: vec![
MenuItem::action("New Tab", OpenNewTab),
MenuItem::action("Split Right", SplitRight),
MenuItem::separator(),
MenuItem::action("Command Mode", FocusCommandMode),
MenuItem::separator(),
+1
View File
@@ -3,6 +3,7 @@ mod internal_pages;
mod plugins;
mod render;
mod site_permissions;
mod splits;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{CommandIntent, ProfileId, SpaceId, TabId, UrlText};
+2 -1
View File
@@ -46,6 +46,7 @@ impl ElyShell {
.on_action(cx.listener(Self::on_restore_closed_tab))
.on_action(cx.listener(Self::on_select_next_tab))
.on_action(cx.listener(Self::on_select_previous_tab))
.on_action(cx.listener(Self::on_split_right))
.on_action(cx.listener(Self::on_toggle_favorite_tab))
.on_action(cx.listener(Self::on_toggle_pinned_tab))
.bg(rgb(ELY_THEME.canvas))
@@ -59,7 +60,7 @@ impl ElyShell {
.flex_1()
.overflow_hidden()
.child(self.render_sidebar(&snapshot, cx))
.child(self.render_web_canvas(&active_tab, &snapshot, cx)),
.child(self.render_content_area(&snapshot, &active_tab, cx)),
)
.into_any_element()
}
+167
View File
@@ -0,0 +1,167 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing};
use ely_domain::{BrowserTab, SplitAxis, SplitLayout};
use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
StatefulInteractiveElement, Styled, Window, div, px, rgb,
};
use gpui_component::{IconName, StyledExt};
use super::{ElyShell, ShellState};
use crate::SplitRight;
impl ElyShell {
pub(super) fn render_content_area(
&mut self,
snapshot: &BrowserSnapshot,
active_tab: &BrowserTab,
cx: &mut Context<Self>,
) -> AnyElement {
let Some(layout) = active_split_layout(snapshot, active_tab) else {
return self.render_web_canvas(active_tab, snapshot, cx);
};
if layout.pane_count() < 2 {
return self.render_web_canvas(active_tab, snapshot, cx);
}
self.render_split_canvas(snapshot, active_tab, layout, cx)
}
pub(super) fn split_right(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state
&& core.split_active_tab_right().is_ok()
{
self.sync_address_input(window, cx);
self.focus_address_bar(window, cx);
cx.notify();
}
}
pub(super) fn on_split_right(
&mut self,
_: &SplitRight,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.split_right(window, cx);
}
fn render_split_canvas(
&mut self,
snapshot: &BrowserSnapshot,
active_tab: &BrowserTab,
layout: &SplitLayout,
cx: &mut Context<Self>,
) -> AnyElement {
let panes = layout
.panes()
.iter()
.filter_map(|pane| snapshot.tabs.iter().find(|tab| tab.id() == pane.tab_id()))
.collect::<Vec<_>>();
if panes.len() < 2 {
return self.render_web_canvas(active_tab, snapshot, cx);
}
let body = div()
.flex_1()
.h_full()
.min_w_0()
.p_3()
.gap_3()
.overflow_hidden()
.bg(rgb(colors::CANVAS));
let body = match layout.axis() {
SplitAxis::Vertical => body.flex().flex_col(),
SplitAxis::Horizontal | SplitAxis::Grid => body.flex(),
};
body.children(
panes
.into_iter()
.enumerate()
.map(|(index, tab)| self.render_split_pane(index, tab, snapshot, cx)),
)
.into_any_element()
}
fn render_split_pane(
&mut self,
index: usize,
tab: &BrowserTab,
snapshot: &BrowserSnapshot,
cx: &mut Context<Self>,
) -> AnyElement {
let active = tab.id() == &snapshot.active_tab_id;
let tab_id = tab.id().clone();
let pane_number = index + 1;
let border = if active { colors::PRIMARY } else { colors::HAIRLINE_STRONG };
let title_color = if active { colors::INK } else { colors::BODY };
div()
.id(SharedString::from(format!("split-pane-{}", tab.id().as_str())))
.flex_1()
.h_full()
.min_w(px(240.0))
.flex()
.flex_col()
.overflow_hidden()
.rounded_md()
.border_1()
.border_color(rgb(border))
.bg(rgb(colors::SURFACE_CARD))
.cursor_pointer()
.hover(|style| style.bg(rgb(colors::CANVAS_SOFT)))
.active(|style| style.opacity(0.92))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_tab(&tab_id, window, cx);
}))
.child(
div()
.h(px(spacing::COMMAND_BAR_HEIGHT - spacing::MD))
.px_3()
.gap_2()
.flex()
.items_center()
.border_b_1()
.border_color(rgb(colors::HAIRLINE))
.bg(rgb(colors::CANVAS_SOFT))
.child(div().text_color(rgb(colors::MUTED)).child(IconName::Frame))
.child(
div()
.text_xs()
.font_semibold()
.text_color(rgb(colors::MUTED))
.child(format!("Pane {pane_number}")),
)
.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_sm()
.font_semibold()
.text_color(rgb(title_color))
.child(tab.title().to_string()),
),
)
.child(
div()
.flex_1()
.min_h_0()
.overflow_hidden()
.child(self.render_web_canvas(tab, snapshot, cx)),
)
.into_any_element()
}
}
fn active_split_layout<'a>(
snapshot: &'a BrowserSnapshot,
active_tab: &BrowserTab,
) -> Option<&'a SplitLayout> {
let split_id = active_tab.split_id()?;
snapshot.split_layouts.iter().find(|layout| layout.id() == split_id)
}