chore: improve tab component

This commit is contained in:
2025-02-05 14:05:32 +07:00
parent 72e42bf22a
commit a941e844b9
8 changed files with 487 additions and 333 deletions

50
Cargo.lock generated
View File

@@ -807,9 +807,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.11" version = "1.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -1039,7 +1039,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.0", "rustc-hash 2.1.0",
@@ -1346,7 +1346,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2059,7 +2059,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2146,7 +2146,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2156,7 +2156,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"gpui", "gpui",
"tokio", "tokio",
@@ -2361,7 +2361,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2851,7 +2851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -3012,7 +3012,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen", "bindgen",
@@ -3191,7 +3191,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#dda112c89422cda6740fdae404e09a227a0f79ce" source = "git+https://github.com/rust-nostr/nostr#e57c6e3733de2799294bd70bf325aaf08a60e4d8"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3219,7 +3219,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#dda112c89422cda6740fdae404e09a227a0f79ce" source = "git+https://github.com/rust-nostr/nostr#e57c6e3733de2799294bd70bf325aaf08a60e4d8"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3231,7 +3231,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#dda112c89422cda6740fdae404e09a227a0f79ce" source = "git+https://github.com/rust-nostr/nostr#e57c6e3733de2799294bd70bf325aaf08a60e4d8"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"nostr", "nostr",
@@ -3241,7 +3241,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#dda112c89422cda6740fdae404e09a227a0f79ce" source = "git+https://github.com/rust-nostr/nostr#e57c6e3733de2799294bd70bf325aaf08a60e4d8"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"heed", "heed",
@@ -3252,7 +3252,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#dda112c89422cda6740fdae404e09a227a0f79ce" source = "git+https://github.com/rust-nostr/nostr#e57c6e3733de2799294bd70bf325aaf08a60e4d8"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3268,7 +3268,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.39.0" version = "0.39.0"
source = "git+https://github.com/rust-nostr/nostr#dda112c89422cda6740fdae404e09a227a0f79ce" source = "git+https://github.com/rust-nostr/nostr#e57c6e3733de2799294bd70bf325aaf08a60e4d8"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4276,7 +4276,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@@ -4405,7 +4405,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -4755,7 +4755,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5080,7 +5080,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -5902,7 +5902,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#28b80455f97dbe21d5d9845b85928feca652c518" source = "git+https://github.com/zed-industries/zed#0963401a8d0e7afed461090cb57be8047e1f79c5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -5927,11 +5927,11 @@ dependencies = [
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.12.1" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.3.1",
"serde", "serde",
"sha1_smol", "sha1_smol",
] ]
@@ -6287,7 +6287,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

@@ -6,9 +6,10 @@ use crate::{
AxisExt as _, StyledExt, AxisExt as _, StyledExt,
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Element, Entity, div, prelude::FluentBuilder as _, px, AnyView, App, AppContext, Axis, Context, Element, Entity,
InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels,
Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window, Point, Render, StatefulInteractiveElement, Style, StyleRefinement, Styled as _, WeakEntity,
Window,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
@@ -355,6 +356,8 @@ impl Render for Dock {
return div(); return div();
} }
let cache_style = StyleRefinement::default().v_flex().size_full();
div() div()
.relative() .relative()
.overflow_hidden() .overflow_hidden()
@@ -369,8 +372,10 @@ impl Render for Dock {
}) })
.map(|this| match &self.panel { .map(|this| match &self.panel {
DockItem::Split { view, .. } => this.child(view.clone()), DockItem::Split { view, .. } => this.child(view.clone()),
DockItem::Tabs { view, .. } => this.child(view.clone()), DockItem::Tabs { view, .. } => {
DockItem::Panel { view, .. } => this.child(view.clone().view()), this.child(AnyView::from(view.clone()).cached(cache_style))
}
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
}) })
.child(self.render_resize_handle(window, cx)) .child(self.render_resize_handle(window, cx))
.child(DockElement { .child(DockElement {

View File

@@ -51,6 +51,27 @@ pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
true true
} }
/// Return false to hide panel, true to show panel, default is `true`.
///
/// This method called in Panel render, we should make sure it is fast.
fn visible(&self, _cx: &App) -> bool {
true
}
/// Set active state of the panel.
///
/// This method will be called when the panel is active or inactive.
///
/// The last_active_panel and current_active_panel will be touched when the panel is active.
fn set_active(&self, _active: bool, _cx: &mut App) {}
/// Set zoomed state of the panel.
///
/// This method will be called when the panel is zoomed or unzoomed.
///
/// Only current Panel will touch this method.
fn set_zoomed(&self, _zoomed: bool, _cx: &mut App) {}
/// The addition popup menu of the panel, default is `None`. /// The addition popup menu of the panel, default is `None`.
fn popup_menu(&self, this: PopupMenu, _cx: &App) -> PopupMenu { fn popup_menu(&self, this: PopupMenu, _cx: &App) -> PopupMenu {
this this
@@ -68,6 +89,9 @@ pub trait PanelView: 'static + Send + Sync {
fn title(&self, cx: &App) -> AnyElement; fn title(&self, cx: &App) -> AnyElement;
fn closable(&self, cx: &App) -> bool; fn closable(&self, cx: &App) -> bool;
fn zoomable(&self, cx: &App) -> bool; fn zoomable(&self, cx: &App) -> bool;
fn visible(&self, cx: &App) -> bool;
fn set_active(&self, active: bool, cx: &mut App);
fn set_zoomed(&self, zoomed: bool, cx: &mut App);
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu; fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu;
fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button>; fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button>;
fn view(&self) -> AnyView; fn view(&self) -> AnyView;
@@ -95,6 +119,21 @@ impl<T: Panel> PanelView for Entity<T> {
self.read(cx).zoomable(cx) self.read(cx).zoomable(cx)
} }
fn visible(&self, cx: &App) -> bool {
self.read(cx).visible(cx)
}
fn set_active(&self, active: bool, cx: &mut App) {
self.update(cx, |this, cx| {
this.set_active(active, cx);
})
}
fn set_zoomed(&self, zoomed: bool, cx: &mut App) {
self.update(cx, |this, cx| {
this.set_zoomed(zoomed, cx);
})
}
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu { fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu {
self.read(cx).popup_menu(menu, cx) self.read(cx).popup_menu(menu, cx)
} }

View File

@@ -177,6 +177,7 @@ impl StackPanel {
fn new_resizable_panel(panel: Arc<dyn PanelView>, size: Option<Pixels>) -> ResizablePanel { fn new_resizable_panel(panel: Arc<dyn PanelView>, size: Option<Pixels>) -> ResizablePanel {
resizable_panel() resizable_panel()
.content_view(panel.view()) .content_view(panel.view())
.content_visible(move |cx| panel.visible(cx))
.when_some(size, |this, size| this.size(size)) .when_some(size, |this, size| this.size(size))
} }

View File

@@ -9,22 +9,24 @@ use crate::{
popup_menu::{PopupMenu, PopupMenuExt}, popup_menu::{PopupMenu, PopupMenuExt},
tab::{tab_bar::TabBar, Tab}, tab::{tab_bar::TabBar, Tab},
theme::{scale::ColorScaleStep, ActiveTheme}, theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt,
}; };
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength, div, img, prelude::FluentBuilder, px, rems, App, AppContext, Context, Corner, DefiniteLength,
DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement as _, IntoElement, ObjectFit, ParentElement, Pixels, Render, ScrollHandle, InteractiveElement as _, IntoElement, ObjectFit, ParentElement, Pixels, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, StyledImage, WeakEntity, Window, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, StyledImage, WeakEntity,
Window,
}; };
use std::sync::Arc; use std::sync::Arc;
#[derive(Clone, Copy)] #[derive(Clone)]
struct TabState { struct TabState {
closable: bool, closable: bool,
zoomable: bool, zoomable: bool,
draggable: bool, draggable: bool,
droppable: bool, droppable: bool,
active_panel: Option<Arc<dyn PanelView>>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -84,7 +86,7 @@ impl Panel for TabPanel {
} }
fn title(&self, cx: &App) -> gpui::AnyElement { fn title(&self, cx: &App) -> gpui::AnyElement {
self.active_panel() self.active_panel(cx)
.map(|panel| panel.title(cx)) .map(|panel| panel.title(cx))
.unwrap_or("Empty Tab".into_any_element()) .unwrap_or("Empty Tab".into_any_element())
} }
@@ -94,19 +96,23 @@ impl Panel for TabPanel {
return false; return false;
} }
self.active_panel() self.active_panel(cx)
.map(|panel| panel.closable(cx)) .map(|panel| panel.closable(cx))
.unwrap_or(true) .unwrap_or(true)
} }
fn zoomable(&self, cx: &App) -> bool { fn zoomable(&self, cx: &App) -> bool {
self.active_panel() self.active_panel(cx)
.map(|panel| panel.zoomable(cx)) .map(|panel| panel.zoomable(cx))
.unwrap_or(false) .unwrap_or(false)
} }
fn visible(&self, cx: &App) -> bool {
self.visible_panels(cx).next().is_some()
}
fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu { fn popup_menu(&self, menu: PopupMenu, cx: &App) -> PopupMenu {
if let Some(panel) = self.active_panel() { if let Some(panel) = self.active_panel(cx) {
panel.popup_menu(menu, cx) panel.popup_menu(menu, cx)
} else { } else {
menu menu
@@ -114,7 +120,7 @@ impl Panel for TabPanel {
} }
fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button> { fn toolbar_buttons(&self, window: &Window, cx: &App) -> Vec<Button> {
if let Some(panel) = self.active_panel() { if let Some(panel) = self.active_panel(cx) {
panel.toolbar_buttons(window, cx) panel.toolbar_buttons(window, cx)
} else { } else {
vec![] vec![]
@@ -147,15 +153,48 @@ impl TabPanel {
self.stack_panel = Some(view); self.stack_panel = Some(view);
} }
/// Return current active_panel View /// Return current active_panel view
pub fn active_panel(&self) -> Option<Arc<dyn PanelView>> { pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
self.panels.get(self.active_ix).cloned() let panel = self.panels.get(self.active_ix);
if let Some(panel) = panel {
if panel.visible(cx) {
Some(panel.clone())
} else {
// Return the first visible panel
self.visible_panels(cx).next()
}
} else {
None
}
} }
fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) { fn set_active_ix(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
if ix == self.active_ix {
return;
}
let last_active_ix = self.active_ix;
self.active_ix = ix; self.active_ix = ix;
self.tab_bar_scroll_handle.scroll_to_item(ix); self.tab_bar_scroll_handle.scroll_to_item(ix);
self.focus_active_panel(window, cx); self.focus_active_panel(window, cx);
// Sync the active state to all panels
cx.spawn(|view, cx| async move {
_ = cx.update(|cx| {
_ = view.update(cx, |view, cx| {
if let Some(last_active) = view.panels.get(last_active_ix) {
last_active.set_active(false, cx);
}
if let Some(active) = view.panels.get(view.active_ix) {
active.set_active(true, cx);
}
});
});
})
.detach();
cx.emit(PanelEvent::LayoutChanged); cx.emit(PanelEvent::LayoutChanged);
cx.notify(); cx.notify();
} }
@@ -332,6 +371,17 @@ impl TabPanel {
self.panels.len() <= 1 self.panels.len() <= 1
} }
/// Return all visible panels
fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
self.panels.iter().filter_map(|panel| {
if panel.visible(cx) {
Some(panel.clone())
} else {
None
}
})
}
/// Return true if the tab panel is draggable. /// Return true if the tab panel is draggable.
/// ///
/// E.g. if the parent and self only have one panel, it is not draggable. /// E.g. if the parent and self only have one panel, it is not draggable.
@@ -348,7 +398,7 @@ impl TabPanel {
fn render_toolbar( fn render_toolbar(
&self, &self,
state: TabState, state: &TabState,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
@@ -382,15 +432,20 @@ impl TabPanel {
.icon(IconName::Ellipsis) .icon(IconName::Ellipsis)
.xsmall() .xsmall()
.ghost() .ghost()
.popup_menu(move |this, _window, cx| { .popup_menu({
build_popup_menu(this, cx) let zoomable = state.zoomable;
.when(state.zoomable, |this| { let closable = state.closable;
let name = if is_zoomed { "Zoom Out" } else { "Zoom In" };
this.separator().menu(name, Box::new(ToggleZoom)) move |this, _window, cx| {
}) build_popup_menu(this, cx)
.when(state.closable, |this| { .when(zoomable, |this| {
this.separator().menu("Close", Box::new(ClosePanel)) let name = if is_zoomed { "Zoom Out" } else { "Zoom In" };
}) this.separator().menu(name, Box::new(ToggleZoom))
})
.when(closable, |this| {
this.separator().menu("Close", Box::new(ClosePanel))
})
}
}) })
.anchor(Corner::TopRight), .anchor(Corner::TopRight),
) )
@@ -481,7 +536,7 @@ impl TabPanel {
fn render_title_bar( fn render_title_bar(
&self, &self,
state: TabState, state: &TabState,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
@@ -499,6 +554,10 @@ impl TabPanel {
if self.panels.len() == 1 && panel_style == PanelStyle::Default { if self.panels.len() == 1 && panel_style == PanelStyle::Default {
let panel = self.panels.first().unwrap(); let panel = self.panels.first().unwrap();
if !panel.visible(cx) {
return div().into_any_element();
}
return h_flex() return h_flex()
.justify_between() .justify_between()
.items_center() .items_center()
@@ -601,47 +660,55 @@ impl TabPanel {
) )
}, },
) )
.children(self.panels.iter().enumerate().map(|(ix, panel)| { .children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
let mut active = ix == self.active_ix; let mut active = state.active_panel.as_ref() == Some(panel);
let disabled = self.is_collapsed; let disabled = self.is_collapsed;
if !panel.visible(cx) {
return None;
}
// Always not show active tab style, if the panel is collapsed // Always not show active tab style, if the panel is collapsed
if self.is_collapsed { if self.is_collapsed {
active = false; active = false;
} }
Tab::new(("tab", ix), panel.title(cx), panel.panel_facepile(cx)) Some(
.py_2() Tab::new(("tab", ix), panel.title(cx), panel.panel_facepile(cx))
.selected(active) .py_2()
.disabled(disabled) .selected(active)
.when(!disabled, |this| { .disabled(disabled)
this.on_click(cx.listener(move |view, _, window, cx| { .when(!disabled, |this| {
view.set_active_ix(ix, window, cx); this.on_click(cx.listener(move |view, _, window, cx| {
})) view.set_active_ix(ix, window, cx);
.when(state.draggable, |this| { }))
this.on_drag( .when(state.draggable, |this| {
DragPanel::new(panel.clone(), view.clone()), this.on_drag(
|drag, _, _, cx| { DragPanel::new(panel.clone(), view.clone()),
cx.stop_propagation(); |drag, _, _, cx| {
cx.new(|_| drag.clone()) cx.stop_propagation();
}, cx.new(|_| drag.clone())
) },
}) )
.when(state.droppable, |this| {
this.drag_over::<DragPanel>(|this, _, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
}) })
.on_drop(cx.listener( .when(state.droppable, |this| {
move |this, drag: &DragPanel, window, cx| { this.drag_over::<DragPanel>(|this, _, _, cx| {
this.will_split_placement = None; this.rounded_l_none()
this.on_drop(drag, Some(ix), true, window, cx) .border_l_2()
}, .border_r_0()
)) .border_color(
}) cx.theme().base.step(cx, ColorScaleStep::FIVE),
}) )
})
.on_drop(cx.listener(
move |this, drag: &DragPanel, window, cx| {
this.will_split_placement = None;
this.on_drop(drag, Some(ix), true, window, cx)
},
))
})
}),
)
})) }))
.child( .child(
// empty space to allow move to last tab right // empty space to allow move to last tab right
@@ -686,7 +753,7 @@ impl TabPanel {
fn render_active_panel( fn render_active_panel(
&self, &self,
state: TabState, state: &TabState,
_window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
@@ -694,73 +761,68 @@ impl TabPanel {
return Empty {}.into_any_element(); return Empty {}.into_any_element();
} }
self.active_panel() let Some(active_panel) = state.active_panel.as_ref() else {
.map(|panel| { return Empty {}.into_any_element();
v_flex() };
.id("tab-content")
.group("") v_flex()
.id("tab-content")
.group("")
.overflow_hidden()
.flex_1()
.p_1()
.child(
div()
.size_full()
.rounded_lg()
.shadow_sm()
.when(cx.theme().appearance.is_dark(), |this| this.shadow_lg())
.bg(cx.theme().background)
.overflow_hidden() .overflow_hidden()
.flex_1() .child(
.p_1() active_panel
.view()
.cached(StyleRefinement::default().v_flex().size_full()),
),
)
.when(state.droppable, |this| {
this.on_drag_move(cx.listener(Self::on_panel_drag_move))
.child( .child(
div() div()
.size_full() .invisible()
.rounded_lg() .absolute()
.shadow_sm() .p_1()
.when(cx.theme().appearance.is_dark(), |this| this.shadow_lg())
.bg(cx.theme().background)
.overflow_hidden()
.child(panel.view()),
)
.when(state.droppable, |this| {
this.on_drag_move(cx.listener(Self::on_panel_drag_move))
.child( .child(
div() div()
.invisible() .rounded_lg()
.absolute() .border_1()
.p_1() .border_color(cx.theme().accent.step(cx, ColorScaleStep::FOUR))
.child( .bg(cx.theme().accent.step_alpha(cx, ColorScaleStep::THREE))
div() .size_full(),
.rounded_lg()
.border_1()
.border_color(
cx.theme().accent.step(cx, ColorScaleStep::FOUR),
)
.bg(cx
.theme()
.accent
.step_alpha(cx, ColorScaleStep::THREE))
.size_full(),
)
.map(|this| match self.will_split_placement {
Some(placement) => {
let size = DefiniteLength::Fraction(0.35);
match placement {
Placement::Left => {
this.left_0().top_0().bottom_0().w(size)
}
Placement::Right => {
this.right_0().top_0().bottom_0().w(size)
}
Placement::Top => {
this.top_0().left_0().right_0().h(size)
}
Placement::Bottom => {
this.bottom_0().left_0().right_0().h(size)
}
}
}
None => this.top_0().left_0().size_full(),
})
.group_drag_over::<DragPanel>("", |this| this.visible())
.on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
this.on_drop(drag, None, true, window, cx)
})),
) )
}) .map(|this| match self.will_split_placement {
.into_any_element() Some(placement) => {
let size = DefiniteLength::Fraction(0.35);
match placement {
Placement::Left => this.left_0().top_0().bottom_0().w(size),
Placement::Right => {
this.right_0().top_0().bottom_0().w(size)
}
Placement::Top => this.top_0().left_0().right_0().h(size),
Placement::Bottom => {
this.bottom_0().left_0().right_0().h(size)
}
}
}
None => this.top_0().left_0().size_full(),
})
.group_drag_over::<DragPanel>("", |this| this.visible())
.on_drop(cx.listener(|this, drag: &DragPanel, window, cx| {
this.on_drop(drag, None, true, window, cx)
})),
)
}) })
.unwrap_or(Empty {}.into_any_element()) .into_any_element()
} }
/// Calculate the split direction based on the current mouse position /// Calculate the split direction based on the current mouse position
@@ -958,7 +1020,7 @@ impl TabPanel {
} }
fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) { fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(active_panel) = self.active_panel() { if let Some(active_panel) = self.active_panel(cx) {
active_panel.focus_handle(cx).focus(window); active_panel.focus_handle(cx).focus(window);
} }
} }
@@ -980,6 +1042,18 @@ impl TabPanel {
} }
self.is_zoomed = !self.is_zoomed; self.is_zoomed = !self.is_zoomed;
cx.spawn(|view, cx| {
let is_zoomed = self.is_zoomed;
async move {
_ = cx.update(|cx| {
_ = view.update(cx, |view, cx| {
view.set_zoomed(is_zoomed, cx);
});
});
}
})
.detach();
} }
fn on_action_close_panel( fn on_action_close_panel(
@@ -988,7 +1062,7 @@ impl TabPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(panel) = self.active_panel() { if let Some(panel) = self.active_panel(cx) {
self.remove_panel(panel, window, cx); self.remove_panel(panel, window, cx);
} }
} }
@@ -996,7 +1070,7 @@ impl TabPanel {
impl Focusable for TabPanel { impl Focusable for TabPanel {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
if let Some(active_panel) = self.active_panel() { if let Some(active_panel) = self.active_panel(cx) {
active_panel.focus_handle(cx) active_panel.focus_handle(cx)
} else { } else {
self.focus_handle.clone() self.focus_handle.clone()
@@ -1011,12 +1085,13 @@ impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel { impl Render for TabPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);
let active_panel = self.active_panel(cx);
let mut state = TabState { let mut state = TabState {
closable: self.closable(cx), closable: self.closable(cx),
draggable: self.draggable(cx), draggable: self.draggable(cx),
droppable: self.droppable(cx), droppable: self.droppable(cx),
zoomable: self.zoomable(cx), zoomable: self.zoomable(cx),
active_panel,
}; };
if !state.draggable { if !state.draggable {
@@ -1030,7 +1105,7 @@ impl Render for TabPanel {
.on_action(cx.listener(Self::on_action_close_panel)) .on_action(cx.listener(Self::on_action_close_panel))
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.child(self.render_title_bar(state, window, cx)) .child(self.render_title_bar(&state, window, cx))
.child(self.render_active_panel(state, window, cx)) .child(self.render_active_panel(&state, window, cx))
} }
} }

View File

@@ -1524,6 +1524,15 @@ impl EntityInputHandler for TextInput {
bounds.origin + end_origin.unwrap_or_default(), bounds.origin + end_origin.unwrap_or_default(),
)) ))
} }
fn character_index_for_point(
&mut self,
_point: gpui::Point<Pixels>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Option<usize> {
None
}
} }
impl Focusable for TextInput { impl Focusable for TextInput {

View File

@@ -304,6 +304,7 @@ impl Render for ResizablePanelGroup {
} }
type ContentBuilder = Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>; type ContentBuilder = Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>;
type ContentVisible = Rc<Box<dyn Fn(&App) -> bool>>;
pub struct ResizablePanel { pub struct ResizablePanel {
group: Option<WeakEntity<ResizablePanelGroup>>, group: Option<WeakEntity<ResizablePanelGroup>>,
@@ -316,6 +317,7 @@ pub struct ResizablePanel {
axis: Axis, axis: Axis,
content_builder: ContentBuilder, content_builder: ContentBuilder,
content_view: Option<AnyView>, content_view: Option<AnyView>,
content_visible: ContentVisible,
/// The bounds of the resizable panel, when render the bounds will be updated. /// The bounds of the resizable panel, when render the bounds will be updated.
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
resize_handle: Option<AnyElement>, resize_handle: Option<AnyElement>,
@@ -331,6 +333,7 @@ impl ResizablePanel {
axis: Axis::Horizontal, axis: Axis::Horizontal,
content_builder: None, content_builder: None,
content_view: None, content_view: None,
content_visible: Rc::new(Box::new(|_| true)),
bounds: Bounds::default(), bounds: Bounds::default(),
resize_handle: None, resize_handle: None,
} }
@@ -344,6 +347,14 @@ impl ResizablePanel {
self self
} }
pub(crate) fn content_visible<F>(mut self, content_visible: F) -> Self
where
F: Fn(&App) -> bool + 'static,
{
self.content_visible = Rc::new(Box::new(content_visible));
self
}
pub fn content_view(mut self, content: AnyView) -> Self { pub fn content_view(mut self, content: AnyView) -> Self {
self.content_view = Some(content); self.content_view = Some(content);
self self
@@ -364,16 +375,14 @@ impl ResizablePanel {
) { ) {
let new_size = bounds.size.along(self.axis); let new_size = bounds.size.along(self.axis);
self.bounds = bounds; self.bounds = bounds;
self.size_ratio = None;
self.size = Some(new_size); self.size = Some(new_size);
let panel_view = cx.entity().clone(); let entity_id = cx.entity_id();
if let Some(group) = self.group.as_ref() { if let Some(group) = self.group.as_ref() {
_ = group.update(cx, |view, _| { _ = group.update(cx, |view, _| {
if let Some(ix) = view if let Some(ix) = view.panels.iter().position(|v| v.entity_id() == entity_id) {
.panels
.iter()
.position(|v| v.entity_id() == panel_view.entity_id())
{
view.sizes[ix] = new_size; view.sizes[ix] = new_size;
} }
}); });
@@ -386,6 +395,14 @@ impl FluentBuilder for ResizablePanel {}
impl Render for ResizablePanel { impl Render for ResizablePanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !(self.content_visible)(cx) {
// To keep size as initial size, to make sure the size will not be changed.
self.initial_size = self.size;
self.size = None;
return div();
}
let view = cx.entity().clone(); let view = cx.entity().clone();
let total_size = self let total_size = self
.group .group

View File

@@ -589,198 +589,206 @@ impl Element for Scrollbar {
let is_visible = self.state.get().is_scrollbar_visible(); let is_visible = self.state.get().is_scrollbar_visible();
let is_hover_to_show = cx.theme().scrollbar_show.is_hover(); let is_hover_to_show = cx.theme().scrollbar_show.is_hover();
for state in prepaint.states.iter() { window.with_content_mask(
let axis = state.axis; Some(ContentMask {
let radius = state.radius; bounds: hitbox_bounds,
let bounds = state.bounds; }),
let thumb_bounds = state.thumb_bounds; |window| {
let scroll_area_size = state.scroll_size; for state in prepaint.states.iter() {
let container_size = state.container_size; let axis = state.axis;
let thumb_size = state.thumb_size; let radius = state.radius;
let margin_end = state.margin_end; let bounds = state.bounds;
let is_vertical = axis.is_vertical(); let thumb_bounds = state.thumb_bounds;
let scroll_area_size = state.scroll_size;
let container_size = state.container_size;
let thumb_size = state.thumb_size;
let margin_end = state.margin_end;
let is_vertical = axis.is_vertical();
window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox); window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
window.paint_layer(hitbox_bounds, |cx| { window.paint_layer(hitbox_bounds, |cx| {
cx.paint_quad(fill(state.bounds, state.bg)); cx.paint_quad(fill(state.bounds, state.bg));
cx.paint_quad(PaintQuad { cx.paint_quad(PaintQuad {
bounds, bounds,
corner_radii: (0.).into(), corner_radii: (0.).into(),
background: gpui::transparent_black().into(), background: gpui::transparent_black().into(),
border_widths: if is_vertical { border_widths: if is_vertical {
Edges { Edges {
top: px(0.), top: px(0.),
right: px(0.), right: px(0.),
bottom: px(0.), bottom: px(0.),
left: BORDER_WIDTH, left: BORDER_WIDTH,
} }
} else {
Edges {
top: BORDER_WIDTH,
right: px(0.),
bottom: px(0.),
left: px(0.),
}
},
border_color: state.border,
});
cx.paint_quad(fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius));
});
window.on_mouse_event({
let state = self.state.clone();
let view_id = self.view_id;
let scroll_handle = self.scroll_handle.clone();
move |event: &ScrollWheelEvent, phase, _, cx| {
if phase.bubble()
&& hitbox_bounds.contains(&event.position)
&& scroll_handle.offset() != state.get().last_scroll_offset
{
state.set(
state
.get()
.with_last_scroll(scroll_handle.offset(), Some(Instant::now())),
);
cx.notify(view_id);
}
}
});
let safe_range = (-scroll_area_size + container_size)..px(0.);
if is_hover_to_show || is_visible {
window.on_mouse_event({
let state = self.state.clone();
let view_id = self.view_id;
let scroll_handle = self.scroll_handle.clone();
move |event: &MouseDownEvent, phase, _, cx| {
if phase.bubble() && bounds.contains(&event.position) {
cx.stop_propagation();
if thumb_bounds.contains(&event.position) {
// click on the thumb bar, set the drag position
let pos = event.position - thumb_bounds.origin;
state.set(state.get().with_drag_pos(axis, pos));
cx.notify(view_id);
} else { } else {
// click on the scrollbar, jump to the position Edges {
// Set the thumb bar center to the click position top: BORDER_WIDTH,
let offset = scroll_handle.offset(); right: px(0.),
let percentage = if is_vertical { bottom: px(0.),
(event.position.y - thumb_size / 2. - bounds.origin.y) left: px(0.),
}
},
border_color: state.border,
});
cx.paint_quad(
fill(state.thumb_fill_bounds, state.thumb_bg).corner_radii(radius),
);
});
window.on_mouse_event({
let state = self.state.clone();
let view_id = self.view_id;
let scroll_handle = self.scroll_handle.clone();
move |event: &ScrollWheelEvent, phase, _, cx| {
if phase.bubble()
&& hitbox_bounds.contains(&event.position)
&& scroll_handle.offset() != state.get().last_scroll_offset
{
state.set(state.get().with_last_scroll(
scroll_handle.offset(),
Some(Instant::now()),
));
cx.notify(view_id);
}
}
});
let safe_range = (-scroll_area_size + container_size)..px(0.);
if is_hover_to_show || is_visible {
window.on_mouse_event({
let state = self.state.clone();
let view_id = self.view_id;
let scroll_handle = self.scroll_handle.clone();
move |event: &MouseDownEvent, phase, _, cx| {
if phase.bubble() && bounds.contains(&event.position) {
cx.stop_propagation();
if thumb_bounds.contains(&event.position) {
// click on the thumb bar, set the drag position
let pos = event.position - thumb_bounds.origin;
state.set(state.get().with_drag_pos(axis, pos));
cx.notify(view_id);
} else {
// click on the scrollbar, jump to the position
// Set the thumb bar center to the click position
let offset = scroll_handle.offset();
let percentage = if is_vertical {
(event.position.y - thumb_size / 2. - bounds.origin.y)
/ (bounds.size.height - thumb_size)
} else {
(event.position.x - thumb_size / 2. - bounds.origin.x)
/ (bounds.size.width - thumb_size)
}
.min(1.);
if is_vertical {
scroll_handle.set_offset(point(
offset.x,
(-scroll_area_size * percentage)
.clamp(safe_range.start, safe_range.end),
));
} else {
scroll_handle.set_offset(point(
(-scroll_area_size * percentage)
.clamp(safe_range.start, safe_range.end),
offset.y,
));
}
}
}
}
});
}
window.on_mouse_event({
let scroll_handle = self.scroll_handle.clone();
let state = self.state.clone();
let view_id = self.view_id;
move |event: &MouseMoveEvent, _, _, cx| {
// Update hovered state for scrollbar
if bounds.contains(&event.position) {
if state.get().hovered_axis != Some(axis) {
state.set(state.get().with_hovered(Some(axis)));
cx.notify(view_id);
}
} else if state.get().hovered_axis == Some(axis)
&& state.get().hovered_axis.is_some()
{
state.set(state.get().with_hovered(None));
cx.notify(view_id);
}
// Update hovered state for scrollbar thumb
if thumb_bounds.contains(&event.position) {
if state.get().hovered_on_thumb != Some(axis) {
state.set(state.get().with_hovered_on_thumb(Some(axis)));
cx.notify(view_id);
}
} else if state.get().hovered_on_thumb == Some(axis) {
state.set(state.get().with_hovered_on_thumb(None));
cx.notify(view_id);
}
// Move thumb position on dragging
if state.get().dragged_axis == Some(axis) && event.dragging() {
// drag_pos is the position of the mouse down event
// We need to keep the thumb bar still at the origin down position
let drag_pos = state.get().drag_pos;
let percentage = (if is_vertical {
(event.position.y - drag_pos.y - bounds.origin.y)
/ (bounds.size.height - thumb_size) / (bounds.size.height - thumb_size)
} else { } else {
(event.position.x - thumb_size / 2. - bounds.origin.x) (event.position.x - drag_pos.x - bounds.origin.x)
/ (bounds.size.width - thumb_size) / (bounds.size.width - thumb_size - margin_end)
} })
.min(1.); .clamp(0., 1.);
if is_vertical { let offset = if is_vertical {
scroll_handle.set_offset(point( point(
offset.x, scroll_handle.offset().x,
(-scroll_area_size * percentage) (-(scroll_area_size - container_size) * percentage)
.clamp(safe_range.start, safe_range.end), .clamp(safe_range.start, safe_range.end),
)); )
} else { } else {
scroll_handle.set_offset(point( point(
(-scroll_area_size * percentage) (-(scroll_area_size - container_size) * percentage)
.clamp(safe_range.start, safe_range.end), .clamp(safe_range.start, safe_range.end),
offset.y, scroll_handle.offset().y,
)); )
};
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|| (scroll_handle.offset().x - offset.x).abs() > px(1.)
{
scroll_handle.set_offset(offset);
cx.notify(view_id);
} }
} }
} }
} });
});
}
window.on_mouse_event({ window.on_mouse_event({
let scroll_handle = self.scroll_handle.clone(); let view_id = self.view_id;
let state = self.state.clone(); let state = self.state.clone();
let view_id = self.view_id;
move |event: &MouseMoveEvent, _, _, cx| { move |_event: &MouseUpEvent, phase, _, cx| {
// Update hovered state for scrollbar if phase.bubble() {
if bounds.contains(&event.position) { state.set(state.get().with_unset_drag_pos());
if state.get().hovered_axis != Some(axis) { cx.notify(view_id);
state.set(state.get().with_hovered(Some(axis))); }
cx.notify(view_id);
} }
} else if state.get().hovered_axis == Some(axis) });
&& state.get().hovered_axis.is_some()
{
state.set(state.get().with_hovered(None));
cx.notify(view_id);
}
// Update hovered state for scrollbar thumb
if thumb_bounds.contains(&event.position) {
if state.get().hovered_on_thumb != Some(axis) {
state.set(state.get().with_hovered_on_thumb(Some(axis)));
cx.notify(view_id);
}
} else if state.get().hovered_on_thumb == Some(axis) {
state.set(state.get().with_hovered_on_thumb(None));
cx.notify(view_id);
}
// Move thumb position on dragging
if state.get().dragged_axis == Some(axis) && event.dragging() {
// drag_pos is the position of the mouse down event
// We need to keep the thumb bar still at the origin down position
let drag_pos = state.get().drag_pos;
let percentage = (if is_vertical {
(event.position.y - drag_pos.y - bounds.origin.y)
/ (bounds.size.height - thumb_size)
} else {
(event.position.x - drag_pos.x - bounds.origin.x)
/ (bounds.size.width - thumb_size - margin_end)
})
.clamp(0., 1.);
let offset = if is_vertical {
point(
scroll_handle.offset().x,
(-(scroll_area_size - container_size) * percentage)
.clamp(safe_range.start, safe_range.end),
)
} else {
point(
(-(scroll_area_size - container_size) * percentage)
.clamp(safe_range.start, safe_range.end),
scroll_handle.offset().y,
)
};
if (scroll_handle.offset().y - offset.y).abs() > px(1.)
|| (scroll_handle.offset().x - offset.x).abs() > px(1.)
{
scroll_handle.set_offset(offset);
cx.notify(view_id);
}
}
} }
}); },
);
window.on_mouse_event({
let view_id = self.view_id;
let state = self.state.clone();
move |_event: &MouseUpEvent, phase, _, cx| {
if phase.bubble() {
state.set(state.get().with_unset_drag_pos());
cx.notify(view_id);
}
}
});
}
} }
} }