Match the design's `0 1px 0 rgba(0,0,0,0.05), 0 12px 30px -16px rgba(0,0,0,0.18)` shadow stack on every split pane card and tighten inter-pane gap from 12 px (`gap_3`) to the design's 10 px. Without the ambient shadow the panes read as flat seams against the wallpaper; with it they sit on the canvas the way the design renders them. Grid axis now also gets a 10 px row gap so the second row doesn't butt up against the first. cargo test --workspace: 440 passed, 0 failed.
447 lines
15 KiB
Rust
447 lines
15 KiB
Rust
use ely_browser_core::BrowserSnapshot;
|
|
use ely_design_system::{colors, spacing};
|
|
use ely_domain::{BrowserTab, SplitAxis, SplitLayout};
|
|
use gpui::{
|
|
AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString,
|
|
StatefulInteractiveElement, Styled, Window, div, hsla, point, px, rgb, rgba,
|
|
};
|
|
use gpui_component::{
|
|
IconName, Selectable, Sizable, StyledExt,
|
|
button::{Button, ButtonVariants},
|
|
};
|
|
|
|
use super::chrome::{
|
|
pane_host_label, pane_url_is_secure, render_compact_split_canvas,
|
|
render_split_pane_header,
|
|
};
|
|
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);
|
|
}
|
|
|
|
pub(super) fn swap_active_split_pane(&mut self, cx: &mut Context<Self>) {
|
|
if let ShellState::Ready(core) = &mut self.state
|
|
&& core.swap_active_split_pane().is_ok_and(|swapped| swapped)
|
|
{
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(super) fn duplicate_active_split_pane(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let ShellState::Ready(core) = &mut self.state
|
|
&& core.duplicate_active_split_pane().is_ok_and(|tab_id| tab_id.is_some())
|
|
{
|
|
self.sync_address_input(window, cx);
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(super) fn detach_active_split_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let ShellState::Ready(core) = &mut self.state
|
|
&& core.detach_active_split_pane().is_ok_and(|detached| detached)
|
|
{
|
|
self.sync_address_input(window, cx);
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(super) fn save_active_split_view(&mut self, cx: &mut Context<Self>) {
|
|
if let ShellState::Ready(core) = &mut self.state
|
|
&& core.save_active_split_view().is_ok_and(|split_id| split_id.is_some())
|
|
{
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(super) fn close_active_saved_split_view(
|
|
&mut self,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let ShellState::Ready(core) = &mut self.state
|
|
&& core.close_active_saved_split_view().is_ok_and(|tab_id| tab_id.is_some())
|
|
{
|
|
self.sync_address_input(window, cx);
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(super) fn set_active_split_axis(&mut self, axis: SplitAxis, cx: &mut Context<Self>) {
|
|
if let ShellState::Ready(core) = &mut self.state
|
|
&& core.set_active_split_axis(axis).is_ok_and(|split_id| split_id.is_some())
|
|
{
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
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(px(14.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.flex_col()
|
|
.overflow_hidden();
|
|
let pane_area = div().flex_1().min_h_0().gap(px(10.0)).overflow_hidden();
|
|
|
|
let compact_canvas = layout.axis() == &SplitAxis::Vertical && layout.pane_count() >= 4;
|
|
let panes = match layout.axis() {
|
|
SplitAxis::Horizontal => pane_area.flex().children(
|
|
panes
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(index, tab)| self.render_split_pane(index, tab, snapshot, false, cx)),
|
|
),
|
|
SplitAxis::Vertical => {
|
|
pane_area.flex().flex_col().children(panes.into_iter().enumerate().map(
|
|
|(index, tab)| self.render_split_pane(index, tab, snapshot, compact_canvas, cx),
|
|
))
|
|
}
|
|
SplitAxis::Grid => self.render_split_grid(pane_area, panes, snapshot, cx),
|
|
};
|
|
|
|
body.child(panes).child(self.render_split_controls(layout.axis(), cx)).into_any_element()
|
|
}
|
|
|
|
fn render_split_grid(
|
|
&mut self,
|
|
body: gpui::Div,
|
|
panes: Vec<&BrowserTab>,
|
|
snapshot: &BrowserSnapshot,
|
|
cx: &mut Context<Self>,
|
|
) -> gpui::Div {
|
|
body.flex().flex_col().gap(px(10.0)).children(
|
|
panes.chunks(2).enumerate().map(|(row_index, row)| {
|
|
div().flex().flex_1().min_h(px(180.0)).gap(px(10.0)).children(
|
|
row.iter().enumerate().map(|(column_index, tab)| {
|
|
let pane_index = row_index * 2 + column_index;
|
|
self.render_split_pane(pane_index, tab, snapshot, false, cx)
|
|
}),
|
|
)
|
|
}),
|
|
)
|
|
}
|
|
|
|
fn render_split_pane(
|
|
&mut self,
|
|
_index: usize,
|
|
tab: &BrowserTab,
|
|
snapshot: &BrowserSnapshot,
|
|
compact_canvas: bool,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let active = tab.id() == &snapshot.active_tab_id;
|
|
let tab_id = tab.id().clone();
|
|
let close_tab_id = tab.id().clone();
|
|
let border = if active { colors::ACCENT } else { colors::HAIRLINE_STRONG };
|
|
let host = pane_host_label(tab);
|
|
let title = tab.title().to_string();
|
|
let secure = pane_url_is_secure(tab);
|
|
|
|
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(px(10.0))
|
|
.border_1()
|
|
.border_color(rgb(border))
|
|
.bg(rgb(0xffffff))
|
|
.shadow(split_pane_shadow())
|
|
.cursor_pointer()
|
|
.hover(|style| style.opacity(0.98))
|
|
.active(|style| style.opacity(0.92))
|
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
shell.select_tab(&tab_id, window, cx);
|
|
}))
|
|
.child(render_split_pane_header(host, title, secure, close_tab_id, cx))
|
|
.child(div().flex_1().min_h_0().overflow_hidden().child(if compact_canvas {
|
|
render_compact_split_canvas(tab)
|
|
} else {
|
|
self.render_web_canvas(tab, snapshot, cx)
|
|
}))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_split_controls(
|
|
&mut self,
|
|
active_axis: &SplitAxis,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
div()
|
|
.min_h(px(36.0))
|
|
.px_2()
|
|
.gap_2()
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.rounded(px(10.0))
|
|
.bg(rgba(colors::GLASS_2))
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.font_semibold()
|
|
.text_color(rgb(colors::MUTED))
|
|
.child("Split Controls"),
|
|
)
|
|
.child(
|
|
div()
|
|
.gap_1()
|
|
.flex()
|
|
.items_center()
|
|
.child(
|
|
div()
|
|
.text_xs()
|
|
.font_semibold()
|
|
.text_color(rgb(colors::MUTED))
|
|
.child("Layout"),
|
|
)
|
|
.child(self.render_split_axis_button(
|
|
"split-axis-horizontal-control",
|
|
SplitAxis::Horizontal,
|
|
active_axis,
|
|
IconName::PanelRight,
|
|
"Horizontal",
|
|
cx,
|
|
))
|
|
.child(self.render_split_axis_button(
|
|
"split-axis-vertical-control",
|
|
SplitAxis::Vertical,
|
|
active_axis,
|
|
IconName::PanelBottom,
|
|
"Vertical",
|
|
cx,
|
|
))
|
|
.child(self.render_split_axis_button(
|
|
"split-axis-grid-control",
|
|
SplitAxis::Grid,
|
|
active_axis,
|
|
IconName::LayoutDashboard,
|
|
"Grid",
|
|
cx,
|
|
)),
|
|
)
|
|
.child(
|
|
Button::new("swap-split-pane-control")
|
|
.ghost()
|
|
.xsmall()
|
|
.icon(IconName::ArrowRight)
|
|
.label("Swap")
|
|
.tooltip("Swap Pane")
|
|
.on_click(cx.listener(|shell, _, _, cx| {
|
|
shell.swap_active_split_pane(cx);
|
|
})),
|
|
)
|
|
.child(
|
|
Button::new("duplicate-split-pane-control")
|
|
.ghost()
|
|
.xsmall()
|
|
.icon(IconName::Copy)
|
|
.label("Duplicate")
|
|
.tooltip("Duplicate Pane")
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.duplicate_active_split_pane(window, cx);
|
|
})),
|
|
)
|
|
.child(
|
|
Button::new("detach-split-pane-control")
|
|
.ghost()
|
|
.xsmall()
|
|
.icon(IconName::PanelRightOpen)
|
|
.label("Detach")
|
|
.tooltip("Detach Pane")
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.detach_active_split_pane(window, cx);
|
|
})),
|
|
)
|
|
.child(
|
|
Button::new("save-split-view-control")
|
|
.ghost()
|
|
.xsmall()
|
|
.icon(IconName::Check)
|
|
.label("Save")
|
|
.tooltip("Save Split View")
|
|
.on_click(cx.listener(|shell, _, _, cx| {
|
|
shell.save_active_split_view(cx);
|
|
})),
|
|
)
|
|
.child(
|
|
Button::new("close-split-view-control")
|
|
.ghost()
|
|
.xsmall()
|
|
.icon(IconName::Close)
|
|
.label("Close")
|
|
.tooltip("Close Saved Split View")
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.close_active_saved_split_view(window, cx);
|
|
})),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_split_axis_button(
|
|
&mut self,
|
|
id: &'static str,
|
|
axis: SplitAxis,
|
|
active_axis: &SplitAxis,
|
|
icon: IconName,
|
|
label: &'static str,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let selected = active_axis == &axis;
|
|
Button::new(id)
|
|
.ghost()
|
|
.xsmall()
|
|
.selected(selected)
|
|
.icon(icon)
|
|
.label(label)
|
|
.tooltip(format!("{label} Split Layout"))
|
|
.on_click(cx.listener(move |shell, _, _, cx| {
|
|
shell.set_active_split_axis(axis.clone(), cx);
|
|
}))
|
|
.into_any_element()
|
|
}
|
|
|
|
pub(super) fn render_saved_split_row(
|
|
&mut self,
|
|
layout: &SplitLayout,
|
|
active: bool,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<AnyElement> {
|
|
let first_tab_id = layout.panes().first()?.tab_id().clone();
|
|
let bg = if active { colors::GLASS_3 } else { 0x00000000 };
|
|
|
|
Some(
|
|
div()
|
|
.id(SharedString::from(format!("saved-split-{}", layout.id().as_str())))
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.bg(rgba(bg))
|
|
.px(px(10.0))
|
|
.py(px(7.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(colors::GLASS_3)))
|
|
.active(|style| style.opacity(0.82))
|
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
shell.select_tab(&first_tab_id, window, cx);
|
|
}))
|
|
.child(div().text_color(rgb(colors::ACCENT)).child(IconName::Frame))
|
|
.child(
|
|
div()
|
|
.min_w_0()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_1()
|
|
.child(
|
|
div()
|
|
.text_size(px(13.0))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.truncate()
|
|
.text_color(rgb(colors::INK))
|
|
.child(layout.title().to_string()),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(px(11.0))
|
|
.text_color(rgb(colors::INK_4))
|
|
.child(format!("{} panes", layout.pane_count())),
|
|
),
|
|
)
|
|
.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)
|
|
}
|
|
|
|
/// Two-layer drop shadow under each split pane card, matching the design's
|
|
/// `boxShadow:'0 1px 0 rgba(0,0,0,0.05), 0 12px 30px -16px rgba(0,0,0,0.18)'`.
|
|
/// The first layer is the hairline that lifts the card off the wallpaper,
|
|
/// the second is the soft ambient lift the design uses to keep stacked
|
|
/// panes legible against any wallpaper theme.
|
|
fn split_pane_shadow() -> Vec<BoxShadow> {
|
|
vec![
|
|
BoxShadow {
|
|
color: hsla(0.0, 0.0, 0.0, 0.05),
|
|
offset: point(px(0.0), px(1.0)),
|
|
blur_radius: px(0.0),
|
|
spread_radius: px(0.0),
|
|
},
|
|
BoxShadow {
|
|
color: hsla(0.0, 0.0, 0.0, 0.18),
|
|
offset: point(px(0.0), px(12.0)),
|
|
blur_radius: px(30.0),
|
|
spread_radius: px(-16.0),
|
|
},
|
|
]
|
|
}
|
|
|