wip
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m50s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m48s
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m50s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m48s
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -1317,7 +1317,6 @@ dependencies = [
|
|||||||
"smol",
|
"smol",
|
||||||
"state",
|
"state",
|
||||||
"theme",
|
"theme",
|
||||||
"title_bar",
|
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ui",
|
"ui",
|
||||||
]
|
]
|
||||||
@@ -1749,6 +1748,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"linicon",
|
||||||
"log",
|
"log",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"theme",
|
"theme",
|
||||||
@@ -6591,20 +6591,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "title_bar"
|
|
||||||
version = "0.3.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"common",
|
|
||||||
"gpui",
|
|
||||||
"linicon",
|
|
||||||
"smallvec",
|
|
||||||
"theme",
|
|
||||||
"ui",
|
|
||||||
"windows 0.61.3",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.49.0"
|
version = "1.49.0"
|
||||||
|
|||||||
3
assets/icons/panel-left-open.svg
Normal file
3
assets/icons/panel-left-open.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.25 4C20.7688 4 22 5.23122 22 6.75V17.25C22 18.7688 20.7688 20 19.25 20H4.75C3.23122 20 2 18.7688 2 17.25V6.75C2 5.23122 3.23122 4 4.75 4H19.25ZM6.25 7.5C5.83579 7.5 5.5 7.83579 5.5 8.25V15.75C5.5 16.1642 5.83579 16.5 6.25 16.5C6.66421 16.5 7 16.1642 7 15.75V8.25C7 7.83579 6.66421 7.5 6.25 7.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 451 B |
3
assets/icons/panel-left.svg
Normal file
3
assets/icons/panel-left.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M2.75 6.75C2.75 5.64543 3.64543 4.75 4.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V6.75Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M6.25 8.25V15.75" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
@@ -30,7 +30,6 @@ icons = [
|
|||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
dock = { path = "../dock" }
|
dock = { path = "../dock" }
|
||||||
title_bar = { path = "../title_bar" }
|
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
state = { path = "../state" }
|
state = { path = "../state" }
|
||||||
|
|||||||
@@ -7,22 +7,25 @@ use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEA
|
|||||||
use dock::panel::{Panel, PanelEvent};
|
use dock::panel::{Panel, PanelEvent};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, relative, uniform_list, App, AppContext, Context, Entity, EventEmitter,
|
deferred, div, relative, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter,
|
||||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
|
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use list_item::RoomListItem;
|
use list_item::RoomListItem;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
|
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
||||||
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::popup_menu::PopupMenuExt;
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
||||||
|
|
||||||
use crate::actions::{RelayStatus, Reload};
|
use crate::actions::{RelayStatus, Reload};
|
||||||
|
use crate::views::compose::compose_button;
|
||||||
|
|
||||||
mod list_item;
|
mod list_item;
|
||||||
|
|
||||||
@@ -589,6 +592,13 @@ impl Focusable for Sidebar {
|
|||||||
|
|
||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
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 {
|
||||||
|
const EMPTY_HELP: &str = "Start a conversation with someone to get started.";
|
||||||
|
const REQUEST_HELP: &str =
|
||||||
|
"New message requests from people you don't know will appear here.";
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let identity = nostr.read(cx).identity();
|
||||||
|
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let loading = chat.read(cx).loading();
|
let loading = chat.read(cx).loading();
|
||||||
|
|
||||||
@@ -619,36 +629,53 @@ impl Render for Sidebar {
|
|||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
|
// Titlebar
|
||||||
|
.child(
|
||||||
|
h_flex().h(TITLEBAR_HEIGHT).w_full().items_center().child(
|
||||||
|
h_flex()
|
||||||
|
.h_6()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.justify_between()
|
||||||
|
.when_some(identity.read(cx).public_key, |this, public_key| {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let profile = persons.read(cx).get(&public_key, cx);
|
||||||
|
|
||||||
|
this.child(
|
||||||
|
Button::new("user")
|
||||||
|
.small()
|
||||||
|
.reverse()
|
||||||
|
.transparent()
|
||||||
|
.icon(IconName::CaretDown)
|
||||||
|
.child(Avatar::new(profile.avatar()).size(rems(1.5))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(div().pr_2p5().child(compose_button())),
|
||||||
|
),
|
||||||
|
)
|
||||||
// Search Input
|
// Search Input
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().px_2p5().child(
|
||||||
.relative()
|
TextInput::new(&self.find_input)
|
||||||
.mt_3()
|
.small()
|
||||||
.px_2p5()
|
.cleanable()
|
||||||
.w_full()
|
.appearance(true)
|
||||||
.h_7()
|
.text_xs()
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.flex()
|
.map(|this| {
|
||||||
.child(
|
if !self.find_input.read(cx).loading {
|
||||||
TextInput::new(&self.find_input)
|
this.suffix(
|
||||||
.small()
|
Button::new("find")
|
||||||
.cleanable()
|
.icon(IconName::Search)
|
||||||
.appearance(true)
|
.tooltip("Press Enter to search")
|
||||||
.text_xs()
|
.transparent()
|
||||||
.map(|this| {
|
.small(),
|
||||||
if !self.find_input.read(cx).loading {
|
)
|
||||||
this.suffix(
|
} else {
|
||||||
Button::new("find")
|
this
|
||||||
.icon(IconName::Search)
|
}
|
||||||
.tooltip("Press Enter to search")
|
}),
|
||||||
.transparent()
|
),
|
||||||
.small(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
// Chat Rooms
|
// Chat Rooms
|
||||||
.child(
|
.child(
|
||||||
@@ -711,14 +738,8 @@ impl Render for Sidebar {
|
|||||||
.ghost()
|
.ghost()
|
||||||
.rounded()
|
.rounded()
|
||||||
.popup_menu(move |this, _window, _cx| {
|
.popup_menu(move |this, _window, _cx| {
|
||||||
this.menu(
|
this.menu("Reload", Box::new(Reload))
|
||||||
"Reload",
|
.menu("Relay Status", Box::new(RelayStatus))
|
||||||
Box::new(Reload),
|
|
||||||
)
|
|
||||||
.menu(
|
|
||||||
"Relay Status",
|
|
||||||
Box::new(RelayStatus),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -746,7 +767,7 @@ impl Render for Sidebar {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.line_height(relative(1.25))
|
.line_height(relative(1.25))
|
||||||
.child(SharedString::from("Start a conversation with someone to get started.")),
|
.child(SharedString::from(EMPTY_HELP)),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
@@ -770,7 +791,7 @@ impl Render for Sidebar {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.line_height(relative(1.25))
|
.line_height(relative(1.25))
|
||||||
.child(SharedString::from("New message requests from people you don't know will appear here.")),
|
.child(SharedString::from(REQUEST_HELP)),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,26 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use auto_update::{AutoUpdateStatus, AutoUpdater};
|
|
||||||
use chat::{ChatEvent, ChatRegistry};
|
use chat::{ChatEvent, ChatRegistry};
|
||||||
use chat_ui::{CopyPublicKey, OpenPublicKey};
|
use chat_ui::{CopyPublicKey, OpenPublicKey};
|
||||||
use common::DEFAULT_SIDEBAR_WIDTH;
|
use common::DEFAULT_SIDEBAR_WIDTH;
|
||||||
use dock::dock::DockPlacement;
|
use dock::dock::DockPlacement;
|
||||||
use dock::{ClosePanel, DockArea, DockItem};
|
use dock::{ClosePanel, DockArea, DockItem};
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity,
|
div, px, relative, App, AppContext, Axis, ClipboardItem, Context, Entity, InteractiveElement,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
StatefulInteractiveElement, Styled, Subscription, Window,
|
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use relay_auth::RelayAuth;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
|
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
|
||||||
use title_bar::TitleBar;
|
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
||||||
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
|
|
||||||
|
|
||||||
use crate::actions::{
|
use crate::actions::{
|
||||||
reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays,
|
reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays,
|
||||||
};
|
};
|
||||||
use crate::user::viewer;
|
use crate::user::viewer;
|
||||||
use crate::views::compose::compose_button;
|
|
||||||
use crate::views::{preferences, setup_relay, welcome};
|
use crate::views::{preferences, setup_relay, welcome};
|
||||||
use crate::{sidebar, user};
|
use crate::{sidebar, user};
|
||||||
|
|
||||||
@@ -39,9 +30,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
/// App's Title Bar
|
|
||||||
title_bar: Entity<TitleBar>,
|
|
||||||
|
|
||||||
/// App's Dock Area
|
/// App's Dock Area
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
@@ -52,8 +40,8 @@ pub struct Workspace {
|
|||||||
impl Workspace {
|
impl Workspace {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let title_bar = cx.new(|_| TitleBar::new());
|
let dock =
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -115,7 +103,6 @@ impl Workspace {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
title_bar,
|
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,44 +340,7 @@ impl Workspace {
|
|||||||
Some(ids)
|
Some(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
/*
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let identity = nostr.read(cx).identity();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.h_6()
|
|
||||||
.w_full()
|
|
||||||
.when_some(identity.read(cx).public_key, |this, public_key| {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let profile = persons.read(cx).get(&public_key, cx);
|
|
||||||
|
|
||||||
this.child(
|
|
||||||
Button::new("user")
|
|
||||||
.small()
|
|
||||||
.reverse()
|
|
||||||
.transparent()
|
|
||||||
.icon(IconName::CaretDown)
|
|
||||||
.child(Avatar::new(profile.avatar()).size(rems(1.5)))
|
|
||||||
.popup_menu(move |this, _window, _cx| {
|
|
||||||
this.label(profile.name())
|
|
||||||
.menu_with_icon("Profile", IconName::Emoji, Box::new(ViewProfile))
|
|
||||||
.menu_with_icon(
|
|
||||||
"Messaging Relays",
|
|
||||||
IconName::Relay,
|
|
||||||
Box::new(ViewRelays),
|
|
||||||
)
|
|
||||||
.separator()
|
|
||||||
.menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode))
|
|
||||||
.menu_with_icon("Themes", IconName::Moon, Box::new(Themes))
|
|
||||||
.menu_with_icon("Settings", IconName::Settings, Box::new(Settings))
|
|
||||||
.menu_with_icon("Sign Out", IconName::Door, Box::new(Logout))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(compose_button())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let status = chat.read(cx).loading();
|
let status = chat.read(cx).loading();
|
||||||
@@ -471,7 +421,7 @@ impl Workspace {
|
|||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Workspace {
|
impl Render for Workspace {
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ gpui.workspace = true
|
|||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
linicon = "2.3.0"
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, App, AppContext, Axis, Context, Element, Entity, InteractiveElement as _, IntoElement,
|
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
|
||||||
StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
|
Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use ui::StyledExt;
|
||||||
use ui::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE};
|
|
||||||
use ui::{AxisExt as _, StyledExt};
|
|
||||||
|
|
||||||
use super::{DockArea, DockItem};
|
use super::{DockArea, DockItem};
|
||||||
use crate::panel::PanelView;
|
use crate::panel::PanelView;
|
||||||
|
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
|
||||||
use crate::tab_panel::TabPanel;
|
use crate::tab_panel::TabPanel;
|
||||||
|
|
||||||
#[derive(Clone, Render)]
|
#[derive(Clone, Render)]
|
||||||
@@ -67,7 +67,7 @@ pub struct Dock {
|
|||||||
pub(super) collapsible: bool,
|
pub(super) collapsible: bool,
|
||||||
|
|
||||||
/// Whether the Dock is resizing
|
/// Whether the Dock is resizing
|
||||||
is_resizing: bool,
|
resizing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dock {
|
impl Dock {
|
||||||
@@ -98,7 +98,7 @@ impl Dock {
|
|||||||
open: true,
|
open: true,
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
size: px(200.0),
|
size: px(200.0),
|
||||||
is_resizing: false,
|
resizing: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,54 +231,16 @@ impl Dock {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let axis = self.placement.axis();
|
let axis = self.placement.axis();
|
||||||
let neg_offset = -HANDLE_PADDING;
|
|
||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
|
|
||||||
div()
|
resize_handle("resize-handle", axis)
|
||||||
.id("resize-handle")
|
.placement(self.placement)
|
||||||
.occlude()
|
|
||||||
.absolute()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.when(self.placement.is_left(), |this| {
|
|
||||||
// FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING)
|
|
||||||
this.cursor_col_resize()
|
|
||||||
.top_0()
|
|
||||||
.right(px(1.))
|
|
||||||
.h_full()
|
|
||||||
.w(HANDLE_SIZE)
|
|
||||||
.pt_12()
|
|
||||||
.pb_4()
|
|
||||||
})
|
|
||||||
.when(self.placement.is_right(), |this| {
|
|
||||||
this.cursor_col_resize()
|
|
||||||
.top_0()
|
|
||||||
.left(px(-0.5))
|
|
||||||
.h_full()
|
|
||||||
.w(HANDLE_SIZE)
|
|
||||||
.pt_12()
|
|
||||||
.pb_4()
|
|
||||||
})
|
|
||||||
.when(self.placement.is_bottom(), |this| {
|
|
||||||
this.cursor_row_resize()
|
|
||||||
.top(neg_offset)
|
|
||||||
.left_0()
|
|
||||||
.w_full()
|
|
||||||
.h(HANDLE_SIZE)
|
|
||||||
.py(HANDLE_PADDING)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.rounded_full()
|
|
||||||
.hover(|this| this.bg(cx.theme().border_variant))
|
|
||||||
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
|
||||||
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
|
||||||
)
|
|
||||||
.on_drag(ResizePanel {}, move |info, _, _, cx| {
|
.on_drag(ResizePanel {}, move |info, _, _, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
view.update(cx, |view, _| {
|
view.update(cx, |view, _cx| {
|
||||||
view.is_resizing = true;
|
view.resizing = true;
|
||||||
});
|
});
|
||||||
cx.new(|_| info.clone())
|
cx.new(|_| info.deref().clone())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +250,7 @@ impl Dock {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if !self.is_resizing {
|
if !self.resizing {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +311,7 @@ impl Dock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn done_resizing(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
fn done_resizing(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||||
self.is_resizing = false;
|
self.resizing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +402,7 @@ impl Element for DockElement {
|
|||||||
) {
|
) {
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let view = self.view.clone();
|
let view = self.view.clone();
|
||||||
let is_resizing = view.read(cx).is_resizing;
|
let is_resizing = view.read(cx).resizing;
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||||
if !is_resizing {
|
if !is_resizing {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges,
|
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity,
|
||||||
Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
|
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
|
||||||
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
|
use ui::ElementExt;
|
||||||
|
|
||||||
use crate::dock::{Dock, DockPlacement};
|
use crate::dock::{Dock, DockPlacement};
|
||||||
use crate::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
use crate::panel::{Panel, PanelEvent, PanelStyle, PanelView};
|
||||||
@@ -14,7 +15,10 @@ use crate::tab_panel::TabPanel;
|
|||||||
|
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
pub mod panel;
|
pub mod panel;
|
||||||
|
mod platforms;
|
||||||
|
pub mod resizable;
|
||||||
pub mod stack_panel;
|
pub mod stack_panel;
|
||||||
|
pub mod tab;
|
||||||
pub mod tab_panel;
|
pub mod tab_panel;
|
||||||
|
|
||||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||||
@@ -30,20 +34,31 @@ pub enum DockEvent {
|
|||||||
/// The main area of the dock.
|
/// The main area of the dock.
|
||||||
pub struct DockArea {
|
pub struct DockArea {
|
||||||
pub(crate) bounds: Bounds<Pixels>,
|
pub(crate) bounds: Bounds<Pixels>,
|
||||||
|
|
||||||
/// The center view of the dockarea.
|
/// The center view of the dockarea.
|
||||||
pub items: DockItem,
|
pub items: DockItem,
|
||||||
/// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed,
|
|
||||||
toggle_button_panels: Edges<Option<EntityId>>,
|
|
||||||
/// The left dock of the dock_area.
|
/// The left dock of the dock_area.
|
||||||
left_dock: Option<Entity<Dock>>,
|
left_dock: Option<Entity<Dock>>,
|
||||||
|
|
||||||
/// The bottom dock of the dock_area.
|
/// The bottom dock of the dock_area.
|
||||||
bottom_dock: Option<Entity<Dock>>,
|
bottom_dock: Option<Entity<Dock>>,
|
||||||
|
|
||||||
/// The right dock of the dock_area.
|
/// The right dock of the dock_area.
|
||||||
right_dock: Option<Entity<Dock>>,
|
right_dock: Option<Entity<Dock>>,
|
||||||
|
|
||||||
|
/// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed,
|
||||||
|
toggle_button_panels: Edges<Option<EntityId>>,
|
||||||
|
|
||||||
|
/// Whether to show the toggle button.
|
||||||
|
toggle_button_visible: bool,
|
||||||
|
|
||||||
/// The top zoom view of the dock_area, if any.
|
/// The top zoom view of the dock_area, if any.
|
||||||
zoom_view: Option<AnyView>,
|
zoom_view: Option<AnyView>,
|
||||||
|
|
||||||
/// Lock panels layout, but allow to resize.
|
/// Lock panels layout, but allow to resize.
|
||||||
is_locked: bool,
|
is_locked: bool,
|
||||||
|
|
||||||
/// The panel style, default is [`PanelStyle::Default`](PanelStyle::Default).
|
/// The panel style, default is [`PanelStyle::Default`](PanelStyle::Default).
|
||||||
pub(crate) panel_style: PanelStyle,
|
pub(crate) panel_style: PanelStyle,
|
||||||
subscriptions: Vec<Subscription>,
|
subscriptions: Vec<Subscription>,
|
||||||
@@ -322,6 +337,7 @@ impl DockArea {
|
|||||||
items: dock_item,
|
items: dock_item,
|
||||||
zoom_view: None,
|
zoom_view: None,
|
||||||
toggle_button_panels: Edges::default(),
|
toggle_button_panels: Edges::default(),
|
||||||
|
toggle_button_visible: true,
|
||||||
left_dock: None,
|
left_dock: None,
|
||||||
right_dock: None,
|
right_dock: None,
|
||||||
bottom_dock: None,
|
bottom_dock: None,
|
||||||
@@ -738,14 +754,7 @@ impl Render for DockArea {
|
|||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
|
||||||
canvas(
|
|
||||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
|
||||||
|_, _, _, _| {},
|
|
||||||
)
|
|
||||||
.absolute()
|
|
||||||
.size_full(),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||||
this.child(zoom_view)
|
this.child(zoom_view)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ impl WindowControl {
|
|||||||
Self { kind, fallback }
|
Self { kind, fallback }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn is_gnome(&self) -> bool {
|
pub fn is_gnome(&self) -> bool {
|
||||||
matches!(detect_desktop_environment(), DesktopEnvironment::Gnome)
|
matches!(detect_desktop_environment(), DesktopEnvironment::Gnome)
|
||||||
}
|
}
|
||||||
@@ -62,8 +63,6 @@ impl WindowControl {
|
|||||||
|
|
||||||
impl RenderOnce for WindowControl {
|
impl RenderOnce for WindowControl {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let is_gnome = self.is_gnome();
|
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(self.kind.as_icon_name())
|
.id(self.kind.as_icon_name())
|
||||||
.group("")
|
.group("")
|
||||||
@@ -71,17 +70,6 @@ impl RenderOnce for WindowControl {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.size_6()
|
.size_6()
|
||||||
.map(|this| {
|
|
||||||
if is_gnome {
|
|
||||||
this.bg(cx.theme().tab_inactive_background)
|
|
||||||
.hover(|this| this.bg(cx.theme().tab_hover_background))
|
|
||||||
.active(|this| this.bg(cx.theme().tab_active_background))
|
|
||||||
} else {
|
|
||||||
this.bg(cx.theme().ghost_element_background)
|
|
||||||
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
|
||||||
.active(|this| this.bg(cx.theme().ghost_element_active))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
|
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
|
||||||
this.child(
|
this.child(
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod linux;
|
pub mod linux;
|
||||||
pub mod mac;
|
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
294
crates/dock/src/resizable/mod.rs
Normal file
294
crates/dock/src/resizable/mod.rs
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod panel;
|
||||||
|
mod resize_handle;
|
||||||
|
pub use panel::*;
|
||||||
|
pub(crate) use resize_handle::*;
|
||||||
|
|
||||||
|
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
|
||||||
|
|
||||||
|
/// Create a [`ResizablePanelGroup`] with horizontal resizing
|
||||||
|
pub fn h_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
|
||||||
|
ResizablePanelGroup::new(id).axis(Axis::Horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [`ResizablePanelGroup`] with vertical resizing
|
||||||
|
pub fn v_resizable(id: impl Into<ElementId>) -> ResizablePanelGroup {
|
||||||
|
ResizablePanelGroup::new(id).axis(Axis::Vertical)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [`ResizablePanel`].
|
||||||
|
pub fn resizable_panel() -> ResizablePanel {
|
||||||
|
ResizablePanel::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for a [`ResizablePanel`]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ResizableState {
|
||||||
|
/// The `axis` will sync to actual axis of the ResizablePanelGroup in use.
|
||||||
|
axis: Axis,
|
||||||
|
panels: Vec<ResizablePanelState>,
|
||||||
|
sizes: Vec<Pixels>,
|
||||||
|
pub(crate) resizing_panel_ix: Option<usize>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ResizableState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
axis: Axis::Horizontal,
|
||||||
|
panels: vec![],
|
||||||
|
sizes: vec![],
|
||||||
|
resizing_panel_ix: None,
|
||||||
|
bounds: Bounds::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResizableState {
|
||||||
|
/// Get the size of the panels.
|
||||||
|
pub fn sizes(&self) -> &Vec<Pixels> {
|
||||||
|
&self.sizes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert_panel(
|
||||||
|
&mut self,
|
||||||
|
size: Option<Pixels>,
|
||||||
|
ix: Option<usize>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let panel_state = ResizablePanelState {
|
||||||
|
size,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = size.unwrap_or(PANEL_MIN_SIZE);
|
||||||
|
|
||||||
|
// We make sure that the size always sums up to the container size
|
||||||
|
// by reducing the size of all other panels first.
|
||||||
|
let container_size = self.container_size().max(px(1.));
|
||||||
|
let total_leftover_size = (container_size - size).max(px(1.));
|
||||||
|
|
||||||
|
for (i, panel) in self.panels.iter_mut().enumerate() {
|
||||||
|
let ratio = self.sizes[i] / container_size;
|
||||||
|
self.sizes[i] = total_leftover_size * ratio;
|
||||||
|
panel.size = Some(self.sizes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ix) = ix {
|
||||||
|
self.panels.insert(ix, panel_state);
|
||||||
|
self.sizes.insert(ix, size);
|
||||||
|
} else {
|
||||||
|
self.panels.push(panel_state);
|
||||||
|
self.sizes.push(size);
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sync_panels_count(
|
||||||
|
&mut self,
|
||||||
|
axis: Axis,
|
||||||
|
panels_count: usize,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let mut changed = self.axis != axis;
|
||||||
|
self.axis = axis;
|
||||||
|
|
||||||
|
if panels_count > self.panels.len() {
|
||||||
|
let diff = panels_count - self.panels.len();
|
||||||
|
self.panels
|
||||||
|
.extend(vec![ResizablePanelState::default(); diff]);
|
||||||
|
self.sizes.extend(vec![PANEL_MIN_SIZE; diff]);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if panels_count < self.panels.len() {
|
||||||
|
self.panels.truncate(panels_count);
|
||||||
|
self.sizes.truncate(panels_count);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
// We need to make sure the total size is in line with the container size.
|
||||||
|
self.adjust_to_container_size(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_panel_size(
|
||||||
|
&mut self,
|
||||||
|
panel_ix: usize,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
size_range: Range<Pixels>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let size = bounds.size.along(self.axis);
|
||||||
|
// This check is only necessary to stop the very first panel from resizing on its own
|
||||||
|
// it needs to be passed when the panel is freshly created so we get the initial size,
|
||||||
|
// but its also fine when it sometimes passes later.
|
||||||
|
if self.sizes[panel_ix].to_f64() == PANEL_MIN_SIZE.to_f64() {
|
||||||
|
self.sizes[panel_ix] = size;
|
||||||
|
self.panels[panel_ix].size = Some(size);
|
||||||
|
}
|
||||||
|
self.panels[panel_ix].bounds = bounds;
|
||||||
|
self.panels[panel_ix].size_range = size_range;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context<Self>) {
|
||||||
|
self.panels.remove(panel_ix);
|
||||||
|
self.sizes.remove(panel_ix);
|
||||||
|
if let Some(resizing_panel_ix) = self.resizing_panel_ix {
|
||||||
|
if resizing_panel_ix > panel_ix {
|
||||||
|
self.resizing_panel_ix = Some(resizing_panel_ix - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.adjust_to_container_size(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn replace_panel(
|
||||||
|
&mut self,
|
||||||
|
panel_ix: usize,
|
||||||
|
panel: ResizablePanelState,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let old_size = self.sizes[panel_ix];
|
||||||
|
|
||||||
|
self.panels[panel_ix] = panel;
|
||||||
|
self.sizes[panel_ix] = old_size;
|
||||||
|
self.adjust_to_container_size(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
self.panels.clear();
|
||||||
|
self.sizes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn container_size(&self) -> Pixels {
|
||||||
|
self.bounds.size.along(self.axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn done_resizing(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.resizing_panel_ix = None;
|
||||||
|
cx.emit(ResizablePanelEvent::Resized);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn panel_size_range(&self, ix: usize) -> Range<Pixels> {
|
||||||
|
let Some(panel) = self.panels.get(ix) else {
|
||||||
|
return PANEL_MIN_SIZE..Pixels::MAX;
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.size_range.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_real_panel_sizes(&mut self, _: &App) {
|
||||||
|
for (i, panel) in self.panels.iter().enumerate() {
|
||||||
|
self.sizes[i] = panel.bounds.size.along(self.axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `ix`` is the index of the panel to resize,
|
||||||
|
/// and the `size` is the new size for the panel.
|
||||||
|
fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let old_sizes = self.sizes.clone();
|
||||||
|
|
||||||
|
let mut ix = ix;
|
||||||
|
// Only resize the left panels.
|
||||||
|
if ix >= old_sizes.len() - 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let container_size = self.container_size();
|
||||||
|
self.sync_real_panel_sizes(cx);
|
||||||
|
|
||||||
|
let move_changed = size - old_sizes[ix];
|
||||||
|
if move_changed == px(0.) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size_range = self.panel_size_range(ix);
|
||||||
|
let new_size = size.clamp(size_range.start, size_range.end);
|
||||||
|
let is_expand = move_changed > px(0.);
|
||||||
|
|
||||||
|
let main_ix = ix;
|
||||||
|
let mut new_sizes = old_sizes.clone();
|
||||||
|
|
||||||
|
if is_expand {
|
||||||
|
let mut changed = new_size - old_sizes[ix];
|
||||||
|
new_sizes[ix] = new_size;
|
||||||
|
|
||||||
|
while changed > px(0.) && ix < old_sizes.len() - 1 {
|
||||||
|
ix += 1;
|
||||||
|
let size_range = self.panel_size_range(ix);
|
||||||
|
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
|
||||||
|
let to_reduce = changed.min(available_size);
|
||||||
|
new_sizes[ix] -= to_reduce;
|
||||||
|
changed -= to_reduce;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut changed = new_size - size;
|
||||||
|
new_sizes[ix] = new_size;
|
||||||
|
|
||||||
|
while changed > px(0.) && ix > 0 {
|
||||||
|
ix -= 1;
|
||||||
|
let size_range = self.panel_size_range(ix);
|
||||||
|
let available_size = (new_sizes[ix] - size_range.start).max(px(0.));
|
||||||
|
let to_reduce = changed.min(available_size);
|
||||||
|
changed -= to_reduce;
|
||||||
|
new_sizes[ix] -= to_reduce;
|
||||||
|
}
|
||||||
|
|
||||||
|
new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_size: Pixels = new_sizes.iter().map(|s| s.to_f64()).sum::<f64>().into();
|
||||||
|
|
||||||
|
// If total size exceeds container size, adjust the main panel
|
||||||
|
if total_size > container_size {
|
||||||
|
let overflow = total_size - container_size;
|
||||||
|
new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, _) in old_sizes.iter().enumerate() {
|
||||||
|
let size = new_sizes[i];
|
||||||
|
self.panels[i].size = Some(size);
|
||||||
|
}
|
||||||
|
self.sizes = new_sizes;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adjust panel sizes according to the container size.
|
||||||
|
///
|
||||||
|
/// When the container size changes, the panels should take up the same percentage as they did before.
|
||||||
|
fn adjust_to_container_size(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.container_size().is_zero() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let container_size = self.container_size();
|
||||||
|
let total_size = px(self.sizes.iter().map(f32::from).sum::<f32>());
|
||||||
|
|
||||||
|
for i in 0..self.panels.len() {
|
||||||
|
let size = self.sizes[i];
|
||||||
|
let ratio = size / total_size;
|
||||||
|
let new_size = container_size * ratio;
|
||||||
|
|
||||||
|
self.sizes[i] = new_size;
|
||||||
|
self.panels[i].size = Some(new_size);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ResizablePanelEvent> for ResizableState {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub(crate) struct ResizablePanelState {
|
||||||
|
pub size: Option<Pixels>,
|
||||||
|
pub size_range: Range<Pixels>,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
}
|
||||||
405
crates/dock/src/resizable/panel.rs
Normal file
405
crates/dock/src/resizable/panel.rs
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
use std::ops::{Deref, Range};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty,
|
||||||
|
Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent,
|
||||||
|
MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window,
|
||||||
|
};
|
||||||
|
use ui::{h_flex, v_flex, AxisExt, ElementExt};
|
||||||
|
|
||||||
|
use super::{resizable_panel, resize_handle, ResizableState};
|
||||||
|
use crate::resizable::PANEL_MIN_SIZE;
|
||||||
|
|
||||||
|
pub enum ResizablePanelEvent {
|
||||||
|
Resized,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct DragPanel;
|
||||||
|
impl Render for DragPanel {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A group of resizable panels.
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ResizablePanelGroup {
|
||||||
|
id: ElementId,
|
||||||
|
state: Option<Entity<ResizableState>>,
|
||||||
|
axis: Axis,
|
||||||
|
size: Option<Pixels>,
|
||||||
|
children: Vec<ResizablePanel>,
|
||||||
|
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResizablePanelGroup {
|
||||||
|
/// Create a new resizable panel group.
|
||||||
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
axis: Axis::Horizontal,
|
||||||
|
children: vec![],
|
||||||
|
state: None,
|
||||||
|
size: None,
|
||||||
|
on_resize: Rc::new(|_, _, _| {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind yourself to a resizable state entity.
|
||||||
|
///
|
||||||
|
/// If not provided, it will handle its own state internally.
|
||||||
|
pub fn with_state(mut self, state: &Entity<ResizableState>) -> Self {
|
||||||
|
self.state = Some(state.clone());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the axis of the resizable panel group, default is horizontal.
|
||||||
|
pub fn axis(mut self, axis: Axis) -> Self {
|
||||||
|
self.axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a panel to the group.
|
||||||
|
///
|
||||||
|
/// - The `axis` will be set to the same axis as the group.
|
||||||
|
/// - The `initial_size` will be set to the average size of all panels if not provided.
|
||||||
|
/// - The `group` will be set to the group entity.
|
||||||
|
pub fn child(mut self, panel: impl Into<ResizablePanel>) -> Self {
|
||||||
|
self.children.push(panel.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple panels to the group.
|
||||||
|
pub fn children<I>(mut self, panels: impl IntoIterator<Item = I>) -> Self
|
||||||
|
where
|
||||||
|
I: Into<ResizablePanel>,
|
||||||
|
{
|
||||||
|
self.children = panels.into_iter().map(|panel| panel.into()).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set size of the resizable panel group
|
||||||
|
///
|
||||||
|
/// - When the axis is horizontal, the size is the height of the group.
|
||||||
|
/// - When the axis is vertical, the size is the width of the group.
|
||||||
|
pub fn size(mut self, size: Pixels) -> Self {
|
||||||
|
self.size = Some(size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the callback to be called when the panels are resized.
|
||||||
|
///
|
||||||
|
/// ## Callback arguments
|
||||||
|
///
|
||||||
|
/// - Entity<ResizableState>: The state of the ResizablePanelGroup.
|
||||||
|
pub fn on_resize(
|
||||||
|
mut self,
|
||||||
|
on_resize: impl Fn(&Entity<ResizableState>, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_resize = Rc::new(on_resize);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for ResizablePanel
|
||||||
|
where
|
||||||
|
T: Into<AnyElement>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
resizable_panel().child(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ResizablePanelGroup> for ResizablePanel {
|
||||||
|
fn from(value: ResizablePanelGroup) -> Self {
|
||||||
|
resizable_panel().child(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
|
||||||
|
|
||||||
|
impl RenderOnce for ResizablePanelGroup {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let state = self.state.unwrap_or(
|
||||||
|
window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()),
|
||||||
|
);
|
||||||
|
let container = if self.axis.is_horizontal() {
|
||||||
|
h_flex()
|
||||||
|
} else {
|
||||||
|
v_flex()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync panels to the state
|
||||||
|
let panels_count = self.children.len();
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.sync_panels_count(self.axis, panels_count, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
container
|
||||||
|
.id(self.id)
|
||||||
|
.size_full()
|
||||||
|
.children(
|
||||||
|
self.children
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, mut panel)| {
|
||||||
|
panel.panel_ix = ix;
|
||||||
|
panel.axis = self.axis;
|
||||||
|
panel.state = Some(state.clone());
|
||||||
|
panel
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_prepaint({
|
||||||
|
let state = state.clone();
|
||||||
|
move |bounds, _, cx| {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
let size_changed =
|
||||||
|
state.bounds.size.along(self.axis) != bounds.size.along(self.axis);
|
||||||
|
|
||||||
|
state.bounds = bounds;
|
||||||
|
|
||||||
|
if size_changed {
|
||||||
|
state.adjust_to_container_size(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(ResizePanelGroupElement {
|
||||||
|
state: state.clone(),
|
||||||
|
axis: self.axis,
|
||||||
|
on_resize: self.on_resize.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resizable panel inside a [`ResizablePanelGroup`].
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ResizablePanel {
|
||||||
|
axis: Axis,
|
||||||
|
panel_ix: usize,
|
||||||
|
state: Option<Entity<ResizableState>>,
|
||||||
|
/// Initial size is the size that the panel has when it is created.
|
||||||
|
initial_size: Option<Pixels>,
|
||||||
|
/// size range limit of this panel.
|
||||||
|
size_range: Range<Pixels>,
|
||||||
|
children: Vec<AnyElement>,
|
||||||
|
visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResizablePanel {
|
||||||
|
/// Create a new resizable panel.
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
panel_ix: 0,
|
||||||
|
initial_size: None,
|
||||||
|
state: None,
|
||||||
|
size_range: (PANEL_MIN_SIZE..Pixels::MAX),
|
||||||
|
axis: Axis::Horizontal,
|
||||||
|
children: vec![],
|
||||||
|
visible: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the visibility of the panel, default is true.
|
||||||
|
pub fn visible(mut self, visible: bool) -> Self {
|
||||||
|
self.visible = visible;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the initial size of the panel.
|
||||||
|
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||||
|
self.initial_size = Some(size.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the size range to limit panel resize.
|
||||||
|
///
|
||||||
|
/// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`].
|
||||||
|
pub fn size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
|
||||||
|
self.size_range = range.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for ResizablePanel {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ResizablePanel {
|
||||||
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
if !self.visible {
|
||||||
|
return div().id(("resizable-panel", self.panel_ix));
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = self
|
||||||
|
.state
|
||||||
|
.expect("BUG: The `state` in ResizablePanel should be present.");
|
||||||
|
let panel_state = state
|
||||||
|
.read(cx)
|
||||||
|
.panels
|
||||||
|
.get(self.panel_ix)
|
||||||
|
.expect("BUG: The `index` of ResizablePanel should be one of in `state`.");
|
||||||
|
let size_range = self.size_range.clone();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(("resizable-panel", self.panel_ix))
|
||||||
|
.flex()
|
||||||
|
.flex_grow()
|
||||||
|
.size_full()
|
||||||
|
.relative()
|
||||||
|
.when(self.axis.is_vertical(), |this| {
|
||||||
|
this.min_h(size_range.start).max_h(size_range.end)
|
||||||
|
})
|
||||||
|
.when(self.axis.is_horizontal(), |this| {
|
||||||
|
this.min_w(size_range.start).max_w(size_range.end)
|
||||||
|
})
|
||||||
|
// 1. initial_size is None, to use auto size.
|
||||||
|
// 2. initial_size is Some and size is none, to use the initial size of the panel for first time render.
|
||||||
|
// 3. initial_size is Some and size is Some, use `size`.
|
||||||
|
.when(self.initial_size.is_none(), |this| this.flex_shrink())
|
||||||
|
.when_some(self.initial_size, |this, initial_size| {
|
||||||
|
// The `self.size` is None, that mean the initial size for the panel,
|
||||||
|
// so we need set `flex_shrink_0` To let it keep the initial size.
|
||||||
|
this.when(
|
||||||
|
panel_state.size.is_none() && !initial_size.is_zero(),
|
||||||
|
|this| this.flex_none(),
|
||||||
|
)
|
||||||
|
.flex_basis(initial_size)
|
||||||
|
})
|
||||||
|
.map(|this| match panel_state.size {
|
||||||
|
Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)),
|
||||||
|
None => this,
|
||||||
|
})
|
||||||
|
.on_prepaint({
|
||||||
|
let state = state.clone();
|
||||||
|
move |bounds, _, cx| {
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
state.update_panel_size(self.panel_ix, bounds, self.size_range, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.children(self.children)
|
||||||
|
.when(self.panel_ix > 0, |this| {
|
||||||
|
let ix = self.panel_ix - 1;
|
||||||
|
this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag(
|
||||||
|
DragPanel,
|
||||||
|
move |drag_panel, _, _, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
// Set current resizing panel ix
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state.resizing_panel_ix = Some(ix);
|
||||||
|
});
|
||||||
|
cx.new(|_| drag_panel.deref().clone())
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
struct ResizePanelGroupElement {
|
||||||
|
state: Entity<ResizableState>,
|
||||||
|
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
|
||||||
|
axis: Axis,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for ResizePanelGroupElement {
|
||||||
|
type Element = Self;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for ResizePanelGroupElement {
|
||||||
|
type PrepaintState = ();
|
||||||
|
type RequestLayoutState = ();
|
||||||
|
|
||||||
|
fn id(&self) -> Option<gpui::ElementId> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
|
(window.request_layout(Style::default(), None, cx), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
_window: &mut Window,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&gpui::GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
_: Bounds<Pixels>,
|
||||||
|
_: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
window.on_mouse_event({
|
||||||
|
let state = self.state.clone();
|
||||||
|
let axis = self.axis;
|
||||||
|
let current_ix = state.read(cx).resizing_panel_ix;
|
||||||
|
move |e: &MouseMoveEvent, phase, window, cx| {
|
||||||
|
if !phase.bubble() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(ix) = current_ix else { return };
|
||||||
|
|
||||||
|
state.update(cx, |state, cx| {
|
||||||
|
let panel = state.panels.get(ix).expect("BUG: invalid panel index");
|
||||||
|
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx)
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
state.resize_panel(ix, e.position.y - panel.bounds.top(), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When any mouse up, stop dragging
|
||||||
|
window.on_mouse_event({
|
||||||
|
let state = self.state.clone();
|
||||||
|
let current_ix = state.read(cx).resizing_panel_ix;
|
||||||
|
let on_resize = self.on_resize.clone();
|
||||||
|
move |_: &MouseUpEvent, phase, window, cx| {
|
||||||
|
if current_ix.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if phase.bubble() {
|
||||||
|
state.update(cx, |state, cx| state.done_resizing(cx));
|
||||||
|
on_resize(&state, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
227
crates/dock/src/resizable/resize_handle.rs
Normal file
227
crates/dock/src/resizable/resize_handle.rs
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
use gpui::{
|
||||||
|
div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId,
|
||||||
|
InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels,
|
||||||
|
Point, Render, StatefulInteractiveElement, Styled as _, Window,
|
||||||
|
};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::AxisExt;
|
||||||
|
|
||||||
|
use crate::dock::DockPlacement;
|
||||||
|
|
||||||
|
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
|
||||||
|
pub(crate) const HANDLE_SIZE: Pixels = px(1.);
|
||||||
|
|
||||||
|
/// Create a resize handle for a resizable panel.
|
||||||
|
pub(crate) fn resize_handle<T: 'static, E: 'static + Render>(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
axis: Axis,
|
||||||
|
) -> ResizeHandle<T, E> {
|
||||||
|
ResizeHandle::new(id, axis)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub(crate) struct ResizeHandle<T: 'static, E: 'static + Render> {
|
||||||
|
id: ElementId,
|
||||||
|
axis: Axis,
|
||||||
|
drag_value: Option<Rc<T>>,
|
||||||
|
placement: Option<DockPlacement>,
|
||||||
|
on_drag: Option<Rc<dyn Fn(&Point<Pixels>, &mut Window, &mut App) -> Entity<E>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static, E: 'static + Render> ResizeHandle<T, E> {
|
||||||
|
fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
|
||||||
|
let id = id.into();
|
||||||
|
Self {
|
||||||
|
id: id.clone(),
|
||||||
|
on_drag: None,
|
||||||
|
drag_value: None,
|
||||||
|
placement: None,
|
||||||
|
axis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_drag(
|
||||||
|
mut self,
|
||||||
|
value: T,
|
||||||
|
f: impl Fn(Rc<T>, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let value = Rc::new(value);
|
||||||
|
self.drag_value = Some(value.clone());
|
||||||
|
self.on_drag = Some(Rc::new(move |p, window, cx| {
|
||||||
|
f(value.clone(), p, window, cx)
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn placement(mut self, placement: DockPlacement) -> Self {
|
||||||
|
self.placement = Some(placement);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
struct ResizeHandleState {
|
||||||
|
active: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResizeHandleState {
|
||||||
|
fn set_active(&self, active: bool) {
|
||||||
|
self.active.set(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
self.active.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static, E: 'static + Render> IntoElement for ResizeHandle<T, E> {
|
||||||
|
type Element = ResizeHandle<T, E>;
|
||||||
|
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static, E: 'static + Render> Element for ResizeHandle<T, E> {
|
||||||
|
type PrepaintState = ();
|
||||||
|
type RequestLayoutState = AnyElement;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<ElementId> {
|
||||||
|
Some(self.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||||
|
let neg_offset = -HANDLE_PADDING;
|
||||||
|
let axis = self.axis;
|
||||||
|
|
||||||
|
window.with_element_state(id.unwrap(), |state, window| {
|
||||||
|
let state = state.unwrap_or(ResizeHandleState::default());
|
||||||
|
|
||||||
|
let bg_color = if state.is_active() {
|
||||||
|
cx.theme().border_variant
|
||||||
|
} else {
|
||||||
|
cx.theme().border
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut el = div()
|
||||||
|
.id(self.id.clone())
|
||||||
|
.occlude()
|
||||||
|
.absolute()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.group("handle")
|
||||||
|
.when_some(self.on_drag.clone(), |this, on_drag| {
|
||||||
|
this.on_drag(
|
||||||
|
self.drag_value.clone().unwrap(),
|
||||||
|
move |_, position, window, cx| on_drag(&position, window, cx),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|this| match self.placement {
|
||||||
|
Some(DockPlacement::Left) => {
|
||||||
|
// Special for Left Dock
|
||||||
|
// FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING)
|
||||||
|
this.cursor_col_resize()
|
||||||
|
.top_0()
|
||||||
|
.right(px(1.))
|
||||||
|
.h_full()
|
||||||
|
.w(HANDLE_SIZE)
|
||||||
|
.pl(HANDLE_PADDING)
|
||||||
|
}
|
||||||
|
_ => this
|
||||||
|
.when(axis.is_horizontal(), |this| {
|
||||||
|
this.cursor_col_resize()
|
||||||
|
.top_0()
|
||||||
|
.left(neg_offset)
|
||||||
|
.h_full()
|
||||||
|
.w(HANDLE_SIZE)
|
||||||
|
.px(HANDLE_PADDING)
|
||||||
|
})
|
||||||
|
.when(axis.is_vertical(), |this| {
|
||||||
|
this.cursor_row_resize()
|
||||||
|
.top(neg_offset)
|
||||||
|
.left_0()
|
||||||
|
.w_full()
|
||||||
|
.h(HANDLE_SIZE)
|
||||||
|
.py(HANDLE_PADDING)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.bg(bg_color)
|
||||||
|
.group_hover("handle", |this| this.bg(bg_color))
|
||||||
|
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
||||||
|
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
||||||
|
)
|
||||||
|
.into_any_element();
|
||||||
|
|
||||||
|
let layout_id = el.request_layout(window, cx);
|
||||||
|
|
||||||
|
((layout_id, el), state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
_: gpui::Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self::PrepaintState {
|
||||||
|
request_layout.prepaint(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&mut self,
|
||||||
|
id: Option<&GlobalElementId>,
|
||||||
|
_: Option<&gpui::InspectorElementId>,
|
||||||
|
bounds: gpui::Bounds<Pixels>,
|
||||||
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
|
_: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
request_layout.paint(window, cx);
|
||||||
|
|
||||||
|
window.with_element_state(id.unwrap(), |state: Option<ResizeHandleState>, window| {
|
||||||
|
let state = state.unwrap_or_default();
|
||||||
|
|
||||||
|
window.on_mouse_event({
|
||||||
|
let state = state.clone();
|
||||||
|
move |ev: &MouseDownEvent, phase, window, _| {
|
||||||
|
if bounds.contains(&ev.position) && phase.bubble() {
|
||||||
|
state.set_active(true);
|
||||||
|
window.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on_mouse_event({
|
||||||
|
let state = state.clone();
|
||||||
|
move |_: &MouseUpEvent, _, window, _| {
|
||||||
|
if state.is_active() {
|
||||||
|
state.set_active(false);
|
||||||
|
window.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
((), state)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity,
|
||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use ui::resizable::{
|
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||||
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
};
|
|
||||||
use ui::{h_flex, AxisExt as _, Placement};
|
use ui::{h_flex, AxisExt as _, Placement};
|
||||||
|
|
||||||
use super::{DockArea, PanelEvent};
|
use super::{DockArea, PanelEvent};
|
||||||
use crate::panel::{Panel, PanelView};
|
use crate::panel::{Panel, PanelView};
|
||||||
|
use crate::resizable::{
|
||||||
|
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
|
||||||
|
PANEL_MIN_SIZE,
|
||||||
|
};
|
||||||
use crate::tab_panel::TabPanel;
|
use crate::tab_panel::TabPanel;
|
||||||
|
|
||||||
pub struct StackPanel {
|
pub struct StackPanel {
|
||||||
@@ -22,9 +22,8 @@ pub struct StackPanel {
|
|||||||
pub(super) axis: Axis,
|
pub(super) axis: Axis,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
pub(crate) panels: SmallVec<[Arc<dyn PanelView>; 2]>,
|
pub(crate) panels: SmallVec<[Arc<dyn PanelView>; 2]>,
|
||||||
panel_group: Entity<ResizablePanelGroup>,
|
state: Entity<ResizableState>,
|
||||||
#[allow(dead_code)]
|
_subscriptions: Vec<Subscription>,
|
||||||
subscriptions: Vec<Subscription>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for StackPanel {
|
impl Panel for StackPanel {
|
||||||
@@ -39,28 +38,23 @@ impl Panel for StackPanel {
|
|||||||
|
|
||||||
impl StackPanel {
|
impl StackPanel {
|
||||||
pub fn new(axis: Axis, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(axis: Axis, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let panel_group = cx.new(|cx| {
|
let state = cx.new(|_| ResizableState::default());
|
||||||
if axis == Axis::Horizontal {
|
|
||||||
h_resizable(window, cx)
|
|
||||||
} else {
|
|
||||||
v_resizable(window, cx)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bubble up the resize event.
|
// Bubble up the resize event.
|
||||||
let subscriptions = vec![cx.subscribe_in(
|
let subscriptions =
|
||||||
&panel_group,
|
vec![
|
||||||
window,
|
cx.subscribe_in(&state, window, |_, _, _: &ResizablePanelEvent, _, cx| {
|
||||||
|_, _, _: &ResizablePanelEvent, _, cx| cx.emit(PanelEvent::LayoutChanged),
|
cx.emit(PanelEvent::LayoutChanged)
|
||||||
)];
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
axis,
|
axis,
|
||||||
parent: None,
|
parent: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
panels: SmallVec::new(),
|
panels: SmallVec::new(),
|
||||||
panel_group,
|
state,
|
||||||
subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,13 +166,6 @@ impl StackPanel {
|
|||||||
self.insert_panel(panel, ix + 1, size, dock_area, window, cx);
|
self.insert_panel(panel, ix + 1, size, dock_area, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_resizable_panel(panel: Arc<dyn PanelView>, size: Option<Pixels>) -> ResizablePanel {
|
|
||||||
resizable_panel()
|
|
||||||
.content_view(panel.view())
|
|
||||||
.content_visible(move |cx| panel.visible(cx))
|
|
||||||
.when_some(size, |this, size| this.size(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_panel(
|
fn insert_panel(
|
||||||
&mut self,
|
&mut self,
|
||||||
panel: Arc<dyn PanelView>,
|
panel: Arc<dyn PanelView>,
|
||||||
@@ -225,14 +212,21 @@ impl StackPanel {
|
|||||||
ix
|
ix
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get avg size of all panels to insert new panel, if size is None.
|
||||||
|
let size = match size {
|
||||||
|
Some(size) => size,
|
||||||
|
None => {
|
||||||
|
let state = self.state.read(cx);
|
||||||
|
(state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert panel
|
||||||
self.panels.insert(ix, panel.clone());
|
self.panels.insert(ix, panel.clone());
|
||||||
self.panel_group.update(cx, |view, cx| {
|
|
||||||
view.insert_child(
|
// Update resizable state
|
||||||
Self::new_resizable_panel(panel.clone(), size),
|
self.state.update(cx, |state, cx| {
|
||||||
ix,
|
state.insert_panel(Some(size), Some(ix), cx);
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
cx.emit(PanelEvent::LayoutChanged);
|
||||||
@@ -240,21 +234,25 @@ impl StackPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove panel from the stack.
|
/// Remove panel from the stack.
|
||||||
|
///
|
||||||
|
/// If `ix` is not found, do nothing.
|
||||||
pub fn remove_panel(
|
pub fn remove_panel(
|
||||||
&mut self,
|
&mut self,
|
||||||
panel: Arc<dyn PanelView>,
|
panel: Arc<dyn PanelView>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(ix) = self.index_of_panel(panel.clone()) {
|
let Some(ix) = self.index_of_panel(panel.clone()) else {
|
||||||
self.panels.remove(ix);
|
return;
|
||||||
self.panel_group.update(cx, |view, cx| {
|
};
|
||||||
view.remove_child(ix, window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
self.panels.remove(ix);
|
||||||
self.remove_self_if_empty(window, cx);
|
self.state.update(cx, |state, cx| {
|
||||||
}
|
state.remove_panel(ix, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.emit(PanelEvent::LayoutChanged);
|
||||||
|
self.remove_self_if_empty(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the old panel with the new panel at same index.
|
/// Replace the old panel with the new panel at same index.
|
||||||
@@ -262,18 +260,14 @@ impl StackPanel {
|
|||||||
&mut self,
|
&mut self,
|
||||||
old_panel: Arc<dyn PanelView>,
|
old_panel: Arc<dyn PanelView>,
|
||||||
new_panel: Entity<StackPanel>,
|
new_panel: Entity<StackPanel>,
|
||||||
window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(ix) = self.index_of_panel(old_panel.clone()) {
|
if let Some(ix) = self.index_of_panel(old_panel.clone()) {
|
||||||
self.panels[ix] = Arc::new(new_panel.clone());
|
self.panels[ix] = Arc::new(new_panel.clone());
|
||||||
self.panel_group.update(cx, |view, cx| {
|
let panel_state = ResizablePanelState::default();
|
||||||
view.replace_child(
|
self.state.update(cx, |state, cx| {
|
||||||
Self::new_resizable_panel(Arc::new(new_panel.clone()), None),
|
state.replace_panel(ix, panel_state, cx);
|
||||||
ix,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
cx.emit(PanelEvent::LayoutChanged);
|
cx.emit(PanelEvent::LayoutChanged);
|
||||||
}
|
}
|
||||||
@@ -362,17 +356,17 @@ impl StackPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove all panels from the stack.
|
/// Remove all panels from the stack.
|
||||||
pub(super) fn remove_all_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.panels.clear();
|
self.panels.clear();
|
||||||
self.panel_group
|
self.state.update(cx, |state, cx| {
|
||||||
.update(cx, |view, cx| view.remove_all_children(window, cx));
|
state.clear();
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the axis of the stack panel.
|
/// Change the axis of the stack panel.
|
||||||
pub(super) fn set_axis(&mut self, axis: Axis, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.axis = axis;
|
self.axis = axis;
|
||||||
self.panel_group
|
|
||||||
.update(cx, |view, cx| view.set_axis(axis, window, cx));
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,10 +382,21 @@ impl EventEmitter<PanelEvent> for StackPanel {}
|
|||||||
impl EventEmitter<DismissEvent> for StackPanel {}
|
impl EventEmitter<DismissEvent> for StackPanel {}
|
||||||
|
|
||||||
impl Render for StackPanel {
|
impl Render for StackPanel {
|
||||||
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 {
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(self.panel_group.clone())
|
.rounded(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.child(
|
||||||
|
ResizablePanelGroup::new("stack-panel-group")
|
||||||
|
.with_state(&self.state)
|
||||||
|
.axis(self.axis)
|
||||||
|
.children(self.panels.clone().into_iter().map(|panel| {
|
||||||
|
resizable_panel()
|
||||||
|
.child(panel.view())
|
||||||
|
.visible(panel.visible(cx))
|
||||||
|
})),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
|
StatefulInteractiveElement, Styled, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
use ui::{Selectable, Sizable, Size};
|
||||||
use crate::Selectable;
|
|
||||||
|
|
||||||
pub mod tab_bar;
|
pub mod tab_bar;
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Tab {
|
pub struct Tab {
|
||||||
base: Stateful<Div>,
|
ix: usize,
|
||||||
label: AnyElement,
|
base: Div,
|
||||||
|
label: Option<AnyElement>,
|
||||||
prefix: Option<AnyElement>,
|
prefix: Option<AnyElement>,
|
||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
size: Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn new(id: impl Into<ElementId>, label: impl IntoElement) -> Self {
|
pub fn new() -> Self {
|
||||||
let id: ElementId = id.into();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base: div().id(id),
|
ix: 0,
|
||||||
label: label.into_any_element(),
|
base: div(),
|
||||||
|
label: None,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
prefix: None,
|
prefix: None,
|
||||||
suffix: None,
|
suffix: None,
|
||||||
|
size: Size::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set label for the tab.
|
||||||
|
pub fn label(mut self, label: impl Into<AnyElement>) -> Self {
|
||||||
|
self.label = Some(label.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the left side of the tab
|
/// Set the left side of the tab
|
||||||
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
|
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
|
||||||
self.prefix = Some(prefix.into());
|
self.prefix = Some(prefix.into());
|
||||||
@@ -50,6 +57,18 @@ impl Tab {
|
|||||||
self.disabled = disabled;
|
self.disabled = disabled;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set index to the tab.
|
||||||
|
pub fn ix(mut self, ix: usize) -> Self {
|
||||||
|
self.ix = ix;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tab {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selectable for Tab {
|
impl Selectable for Tab {
|
||||||
@@ -77,6 +96,13 @@ impl Styled for Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sizable for Tab {
|
||||||
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
|
self.size = size.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RenderOnce for Tab {
|
impl RenderOnce for Tab {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) {
|
let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) {
|
||||||
@@ -103,6 +129,7 @@ impl RenderOnce for Tab {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
|
.id(self.ix)
|
||||||
.h(px(30.))
|
.h(px(30.))
|
||||||
.px_2()
|
.px_2()
|
||||||
.relative()
|
.relative()
|
||||||
@@ -115,12 +142,11 @@ impl RenderOnce for Tab {
|
|||||||
.text_ellipsis()
|
.text_ellipsis()
|
||||||
.text_color(text_color)
|
.text_color(text_color)
|
||||||
.bg(bg_color)
|
.bg(bg_color)
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.hover(|this| this.bg(hover_bg_color))
|
.hover(|this| this.bg(hover_bg_color))
|
||||||
.when_some(self.prefix, |this, prefix| {
|
.when_some(self.prefix, |this, prefix| {
|
||||||
this.child(prefix).text_color(text_color)
|
this.child(prefix).text_color(text_color)
|
||||||
})
|
})
|
||||||
.child(self.label)
|
.when_some(self.label, |this, label| this.child(label))
|
||||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
143
crates/dock/src/tab/tab_bar.rs
Normal file
143
crates/dock/src/tab/tab_bar.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
use gpui::prelude::FluentBuilder as _;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use gpui::Pixels;
|
||||||
|
use gpui::{
|
||||||
|
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
|
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::{h_flex, Sizable, Size, StyledExt};
|
||||||
|
|
||||||
|
use crate::platforms::linux::LinuxWindowControls;
|
||||||
|
use crate::platforms::windows::WindowsWindowControls;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct TabBar {
|
||||||
|
base: Div,
|
||||||
|
style: StyleRefinement,
|
||||||
|
scroll_handle: Option<ScrollHandle>,
|
||||||
|
prefix: Option<AnyElement>,
|
||||||
|
suffix: Option<AnyElement>,
|
||||||
|
last_empty_space: AnyElement,
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
size: Size,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabBar {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
base: div().px(px(-1.)),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
|
scroll_handle: None,
|
||||||
|
children: SmallVec::new(),
|
||||||
|
prefix: None,
|
||||||
|
suffix: None,
|
||||||
|
size: Size::default(),
|
||||||
|
last_empty_space: div().w_3().into_any_element(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track the scroll of the TabBar.
|
||||||
|
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
|
||||||
|
self.scroll_handle = Some(scroll_handle.clone());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the prefix element of the TabBar
|
||||||
|
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||||
|
self.prefix = Some(prefix.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the suffix element of the TabBar
|
||||||
|
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
||||||
|
self.suffix = Some(suffix.into_any_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the last empty space element of the TabBar.
|
||||||
|
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
|
||||||
|
self.last_empty_space = last_empty_space.into_any_element();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn height(window: &mut Window) -> Pixels {
|
||||||
|
(1.75 * window.rem_size()).max(px(36.))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TabBar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for TabBar {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled for TabBar {
|
||||||
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
|
&mut self.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sizable for TabBar {
|
||||||
|
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||||
|
self.size = size.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for TabBar {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let focused = window.is_window_active();
|
||||||
|
|
||||||
|
self.base
|
||||||
|
.group("tab-bar")
|
||||||
|
.refine_style(&self.style)
|
||||||
|
.relative()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.when(!focused, |this| {
|
||||||
|
this.bg(cx.theme().elevated_surface_background.opacity(0.75))
|
||||||
|
})
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.overflow_hidden()
|
||||||
|
.text_color(cx.theme().text)
|
||||||
|
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.id("tabs")
|
||||||
|
.flex_grow()
|
||||||
|
.gap_1()
|
||||||
|
.overflow_x_scroll()
|
||||||
|
.when_some(self.scroll_handle, |this, scroll_handle| {
|
||||||
|
this.track_scroll(&scroll_handle)
|
||||||
|
})
|
||||||
|
.children(self.children)
|
||||||
|
.when(self.suffix.is_some(), |this| {
|
||||||
|
this.child(self.last_empty_space)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
||||||
|
.when(
|
||||||
|
!cx.theme().platform.is_mac() && !window.is_fullscreen(),
|
||||||
|
|this| match cx.theme().platform {
|
||||||
|
theme::PlatformKind::Linux => {
|
||||||
|
this.child(div().px_2().child(LinuxWindowControls::new()))
|
||||||
|
}
|
||||||
|
theme::PlatformKind::Windows => {
|
||||||
|
this.child(WindowsWindowControls::new(Self::height(window)))
|
||||||
|
}
|
||||||
|
_ => this,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,18 @@ use gpui::{
|
|||||||
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
|
div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent,
|
||||||
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement,
|
||||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
StatefulInteractiveElement, Styled, WeakEntity, Window, WindowControlArea,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
|
||||||
use ui::button::{Button, ButtonVariants as _};
|
use ui::button::{Button, ButtonVariants as _};
|
||||||
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
||||||
use ui::tab::tab_bar::TabBar;
|
|
||||||
use ui::tab::Tab;
|
|
||||||
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
||||||
|
|
||||||
|
use crate::dock::DockPlacement;
|
||||||
use crate::panel::{Panel, PanelView};
|
use crate::panel::{Panel, PanelView};
|
||||||
use crate::stack_panel::StackPanel;
|
use crate::stack_panel::StackPanel;
|
||||||
|
use crate::tab::tab_bar::TabBar;
|
||||||
|
use crate::tab::Tab;
|
||||||
use crate::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
use crate::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -64,16 +65,32 @@ impl Render for DragPanel {
|
|||||||
pub struct TabPanel {
|
pub struct TabPanel {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
/// The stock_panel can be None, if is None, that means the panels can't be split or move
|
|
||||||
stack_panel: Option<WeakEntity<StackPanel>>,
|
/// List of panels in the tab panel
|
||||||
pub(crate) panels: Vec<Arc<dyn PanelView>>,
|
pub(crate) panels: Vec<Arc<dyn PanelView>>,
|
||||||
|
|
||||||
|
/// Current active panel index
|
||||||
pub(crate) active_ix: usize,
|
pub(crate) active_ix: usize,
|
||||||
|
|
||||||
/// If this is true, the Panel closeable will follow the active panel's closeable,
|
/// If this is true, the Panel closeable will follow the active panel's closeable,
|
||||||
/// otherwise this TabPanel will not able to close
|
/// otherwise this TabPanel will not able to close
|
||||||
pub(crate) closable: bool,
|
pub(crate) closable: bool,
|
||||||
|
|
||||||
|
/// The stock_panel can be None, if is None, that means the panels can't be split or move
|
||||||
|
stack_panel: Option<WeakEntity<StackPanel>>,
|
||||||
|
|
||||||
|
/// Scroll handle for the tab bar
|
||||||
tab_bar_scroll_handle: ScrollHandle,
|
tab_bar_scroll_handle: ScrollHandle,
|
||||||
is_zoomed: bool,
|
|
||||||
is_collapsed: bool,
|
/// Whether the tab panel is zoomeds
|
||||||
|
zoomed: bool,
|
||||||
|
|
||||||
|
/// Whether the tab panel is collapsed
|
||||||
|
collapsed: bool,
|
||||||
|
|
||||||
|
/// Whether window is moving
|
||||||
|
window_move: bool,
|
||||||
|
|
||||||
/// When drag move, will get the placement of the panel to be split
|
/// When drag move, will get the placement of the panel to be split
|
||||||
will_split_placement: Option<Placement>,
|
will_split_placement: Option<Placement>,
|
||||||
}
|
}
|
||||||
@@ -141,8 +158,9 @@ impl TabPanel {
|
|||||||
active_ix: 0,
|
active_ix: 0,
|
||||||
tab_bar_scroll_handle: ScrollHandle::new(),
|
tab_bar_scroll_handle: ScrollHandle::new(),
|
||||||
will_split_placement: None,
|
will_split_placement: None,
|
||||||
is_zoomed: false,
|
zoomed: false,
|
||||||
is_collapsed: false,
|
collapsed: false,
|
||||||
|
window_move: false,
|
||||||
closable: true,
|
closable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,7 +356,7 @@ impl TabPanel {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
self.is_collapsed = collapsed;
|
self.collapsed = collapsed;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +369,7 @@ impl TabPanel {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_zoomed {
|
if self.zoomed {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +425,7 @@ impl TabPanel {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let is_zoomed = self.is_zoomed && state.zoomable;
|
let is_zoomed = self.zoomed && state.zoomable;
|
||||||
let view = cx.entity().clone();
|
let view = cx.entity().clone();
|
||||||
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
|
||||||
let toolbar = self.toolbar_buttons(window, cx);
|
let toolbar = self.toolbar_buttons(window, cx);
|
||||||
@@ -419,7 +437,7 @@ impl TabPanel {
|
|||||||
.occlude()
|
.occlude()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
|
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
|
||||||
.when(self.is_zoomed, |this| {
|
.when(self.zoomed, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
Button::new("zoom")
|
Button::new("zoom")
|
||||||
.icon(IconName::Zoom)
|
.icon(IconName::Zoom)
|
||||||
@@ -432,8 +450,7 @@ impl TabPanel {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(has_toolbar, |this| {
|
.when(has_toolbar, |this| {
|
||||||
this.bg(cx.theme().surface_background)
|
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
||||||
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
Button::new("menu")
|
Button::new("menu")
|
||||||
@@ -460,21 +477,115 @@ impl TabPanel {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_dock_toggle_button(
|
||||||
|
&self,
|
||||||
|
placement: DockPlacement,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<Button> {
|
||||||
|
if self.zoomed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dock_area = self.dock_area.upgrade()?.read(cx);
|
||||||
|
|
||||||
|
if !dock_area.toggle_button_visible {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dock_area.is_dock_collapsible(placement, cx) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let view_entity_id = cx.entity().entity_id();
|
||||||
|
let toggle_button_panels = dock_area.toggle_button_panels;
|
||||||
|
|
||||||
|
// Check if current TabPanel's entity_id matches the one stored in DockArea for this placement
|
||||||
|
if !match placement {
|
||||||
|
DockPlacement::Left => {
|
||||||
|
dock_area.left_dock.is_some() && toggle_button_panels.left == Some(view_entity_id)
|
||||||
|
}
|
||||||
|
DockPlacement::Right => {
|
||||||
|
dock_area.right_dock.is_some() && toggle_button_panels.right == Some(view_entity_id)
|
||||||
|
}
|
||||||
|
DockPlacement::Bottom => {
|
||||||
|
dock_area.bottom_dock.is_some()
|
||||||
|
&& toggle_button_panels.bottom == Some(view_entity_id)
|
||||||
|
}
|
||||||
|
DockPlacement::Center => unreachable!(),
|
||||||
|
} {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_open = dock_area.is_dock_open(placement, cx);
|
||||||
|
|
||||||
|
let icon = match placement {
|
||||||
|
DockPlacement::Left => {
|
||||||
|
if is_open {
|
||||||
|
IconName::PanelLeft
|
||||||
|
} else {
|
||||||
|
IconName::PanelLeftOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockPlacement::Right => {
|
||||||
|
if is_open {
|
||||||
|
IconName::PanelRight
|
||||||
|
} else {
|
||||||
|
IconName::PanelRightOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockPlacement::Bottom => {
|
||||||
|
if is_open {
|
||||||
|
IconName::PanelBottom
|
||||||
|
} else {
|
||||||
|
IconName::PanelBottomOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DockPlacement::Center => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(
|
||||||
|
Button::new(SharedString::from(format!("toggle-dock:{:?}", placement)))
|
||||||
|
.icon(icon)
|
||||||
|
.small()
|
||||||
|
.ghost()
|
||||||
|
.tab_stop(false)
|
||||||
|
.tooltip(match is_open {
|
||||||
|
true => "Collapse",
|
||||||
|
false => "Expand",
|
||||||
|
})
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let dock_area = self.dock_area.clone();
|
||||||
|
move |_this, _ev, window, cx| {
|
||||||
|
_ = dock_area.update(cx, |dock_area, cx| {
|
||||||
|
dock_area.toggle_dock(placement, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
let view = cx.entity().clone();
|
// Get the dock area entity
|
||||||
|
|
||||||
let Some(dock_area) = self.dock_area.upgrade() else {
|
let Some(dock_area) = self.dock_area.upgrade() else {
|
||||||
|
// Return a default element if the dock area is not available
|
||||||
return div().into_any_element();
|
return div().into_any_element();
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel_style = dock_area.read(cx).panel_style;
|
#[cfg(target_os = "linux")]
|
||||||
|
let supported_controls = window.window_controls();
|
||||||
|
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
|
||||||
|
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
|
||||||
|
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
|
||||||
|
let has_extend_dock_button = left_dock_button.is_some() || bottom_dock_button.is_some();
|
||||||
|
let tabs_count = self.panels.len();
|
||||||
|
|
||||||
if self.panels.len() == 1 && panel_style == PanelStyle::Default {
|
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
|
||||||
let panel = self.panels.first().unwrap();
|
let panel = self.panels.first().unwrap();
|
||||||
|
|
||||||
if !panel.visible(cx) {
|
if !panel.visible(cx) {
|
||||||
@@ -487,7 +598,21 @@ impl TabPanel {
|
|||||||
.line_height(rems(1.0))
|
.line_height(rems(1.0))
|
||||||
.h(px(30.))
|
.h(px(30.))
|
||||||
.py_2()
|
.py_2()
|
||||||
.px_3()
|
.pl_3()
|
||||||
|
.pr_2()
|
||||||
|
.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
.when(left_dock_button.is_some(), |this| this.pl_2())
|
||||||
|
.when(right_dock_button.is_some(), |this| this.pr_2())
|
||||||
|
.when(has_extend_dock_button, |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.mr_1()
|
||||||
|
.gap_1()
|
||||||
|
.children(left_dock_button)
|
||||||
|
.children(bottom_dock_button),
|
||||||
|
)
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.id("tab")
|
.id("tab")
|
||||||
@@ -506,7 +631,7 @@ impl TabPanel {
|
|||||||
this.on_drag(
|
this.on_drag(
|
||||||
DragPanel {
|
DragPanel {
|
||||||
panel: panel.clone(),
|
panel: panel.clone(),
|
||||||
tab_panel: view,
|
tab_panel: cx.entity(),
|
||||||
},
|
},
|
||||||
|drag, _, _, cx| {
|
|drag, _, _, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
@@ -525,25 +650,46 @@ impl TabPanel {
|
|||||||
.into_any_element();
|
.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
let tabs_count = self.panels.len();
|
TabBar::new()
|
||||||
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
TabBar::new("tab-bar")
|
.h(TITLEBAR_HEIGHT)
|
||||||
.track_scroll(self.tab_bar_scroll_handle.clone())
|
.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
.when(self.zoomed, |this| {
|
||||||
|
this.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(has_extend_dock_button, |this| {
|
||||||
|
this.prefix(
|
||||||
|
h_flex()
|
||||||
|
.items_center()
|
||||||
|
.top_0()
|
||||||
|
.right(-px(1.))
|
||||||
|
.border_r_1()
|
||||||
|
.h_full()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.px_2()
|
||||||
|
.children(left_dock_button)
|
||||||
|
.children(bottom_dock_button),
|
||||||
|
)
|
||||||
|
})
|
||||||
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
|
||||||
|
let disabled = self.collapsed;
|
||||||
let mut active = state.active_panel.as_ref() == Some(panel);
|
let mut active = state.active_panel.as_ref() == Some(panel);
|
||||||
let disabled = self.is_collapsed;
|
|
||||||
|
|
||||||
|
// If the panel is not visible, hide the tabbar
|
||||||
if !panel.visible(cx) {
|
if !panel.visible(cx) {
|
||||||
return None;
|
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.collapsed {
|
||||||
active = false;
|
active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
Tab::new(("tab", ix), panel.title(cx))
|
Tab::new()
|
||||||
|
.ix(ix)
|
||||||
|
.label(panel.title(cx))
|
||||||
.py_2()
|
.py_2()
|
||||||
.selected(active)
|
.selected(active)
|
||||||
.disabled(disabled)
|
.disabled(disabled)
|
||||||
@@ -562,7 +708,7 @@ impl TabPanel {
|
|||||||
}))
|
}))
|
||||||
.when(state.draggable, |this| {
|
.when(state.draggable, |this| {
|
||||||
this.on_drag(
|
this.on_drag(
|
||||||
DragPanel::new(panel.clone(), view.clone()),
|
DragPanel::new(panel.clone(), cx.entity().clone()),
|
||||||
|drag, _, _, cx| {
|
|drag, _, _, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.new(|_| drag.clone())
|
cx.new(|_| drag.clone())
|
||||||
@@ -586,16 +732,48 @@ impl TabPanel {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
.child(
|
.last_empty_space(
|
||||||
// empty space to allow move to last tab right
|
// empty space to allow move to last tab right
|
||||||
div()
|
div()
|
||||||
.id("tab-bar-empty-space")
|
.id("tab-bar-empty-space")
|
||||||
|
.window_control_area(WindowControlArea::Drag)
|
||||||
.h_full()
|
.h_full()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.min_w_16()
|
.min_w_16()
|
||||||
.rounded(cx.theme().radius)
|
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
||||||
|
PlatformKind::Linux => this
|
||||||
|
.when(supported_controls.window_menu, |this| {
|
||||||
|
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
|
||||||
|
window.show_window_menu(ev.position)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
||||||
|
if this.window_move {
|
||||||
|
this.window_move = false;
|
||||||
|
window.start_window_move();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
|
||||||
|
this.window_move = false;
|
||||||
|
}))
|
||||||
|
.on_mouse_up(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |this, _ev, _window, _cx| {
|
||||||
|
this.window_move = false;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |this, _ev, _window, _cx| {
|
||||||
|
this.window_move = true;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ => this,
|
||||||
|
})
|
||||||
.when(state.droppable, |this| {
|
.when(state.droppable, |this| {
|
||||||
this.drag_over::<DragPanel>(|this, _, _, cx| {
|
let view = cx.entity();
|
||||||
|
|
||||||
|
this.drag_over::<DragPanel>(|this, _d, _window, cx| {
|
||||||
this.bg(cx.theme().surface_background)
|
this.bg(cx.theme().surface_background)
|
||||||
})
|
})
|
||||||
.on_drop(cx.listener(
|
.on_drop(cx.listener(
|
||||||
@@ -632,7 +810,7 @@ impl TabPanel {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
if self.is_collapsed {
|
if self.collapsed {
|
||||||
return Empty {}.into_any_element();
|
return Empty {}.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,16 +1083,16 @@ impl TabPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.is_zoomed {
|
if !self.zoomed {
|
||||||
cx.emit(PanelEvent::ZoomIn)
|
cx.emit(PanelEvent::ZoomIn)
|
||||||
} else {
|
} else {
|
||||||
cx.emit(PanelEvent::ZoomOut)
|
cx.emit(PanelEvent::ZoomOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.is_zoomed = !self.is_zoomed;
|
self.zoomed = !self.zoomed;
|
||||||
|
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let is_zoomed = self.is_zoomed;
|
let is_zoomed = self.zoomed;
|
||||||
async move |view, cx| {
|
async move |view, cx| {
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
view.set_zoomed(is_zoomed, cx);
|
view.set_zoomed(is_zoomed, cx);
|
||||||
@@ -948,7 +1126,6 @@ impl Focusable for TabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||||
|
|
||||||
impl Render for TabPanel {
|
impl Render for TabPanel {
|
||||||
@@ -969,11 +1146,12 @@ impl Render for TabPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.when(!self.is_collapsed, |this| {
|
.when(!self.collapsed, |this| {
|
||||||
this.on_action(cx.listener(Self::on_action_toggle_zoom))
|
this.on_action(cx.listener(Self::on_action_toggle_zoom))
|
||||||
.on_action(cx.listener(Self::on_action_close_panel))
|
.on_action(cx.listener(Self::on_action_close_panel))
|
||||||
})
|
})
|
||||||
.id("tab-panel")
|
.id("tab-panel")
|
||||||
|
.tab_group()
|
||||||
.track_focus(&focus_handle)
|
.track_focus(&focus_handle)
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ impl ThemeColors {
|
|||||||
background: neutral().light().step_1(),
|
background: neutral().light().step_1(),
|
||||||
surface_background: neutral().light().step_2(),
|
surface_background: neutral().light().step_2(),
|
||||||
elevated_surface_background: neutral().light().step_3(),
|
elevated_surface_background: neutral().light().step_3(),
|
||||||
panel_background: gpui::white(),
|
panel_background: neutral().light().step_4(),
|
||||||
overlay: neutral().light_alpha().step_3(),
|
overlay: neutral().light_alpha().step_3(),
|
||||||
title_bar: gpui::transparent_black(),
|
title_bar: gpui::transparent_black(),
|
||||||
title_bar_inactive: neutral().light().step_1(),
|
title_bar_inactive: neutral().light().step_1(),
|
||||||
@@ -164,9 +164,9 @@ impl ThemeColors {
|
|||||||
ghost_element_selected: neutral().light().step_5(),
|
ghost_element_selected: neutral().light().step_5(),
|
||||||
ghost_element_disabled: neutral().light_alpha().step_2(),
|
ghost_element_disabled: neutral().light_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().light().step_3(),
|
tab_inactive_background: neutral().light().step_2(),
|
||||||
tab_hover_background: neutral().light().step_4(),
|
tab_hover_background: neutral().light().step_3(),
|
||||||
tab_active_background: neutral().light().step_5(),
|
tab_active_background: neutral().light().step_4(),
|
||||||
|
|
||||||
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
scrollbar_thumb_background: neutral().light_alpha().step_3(),
|
||||||
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
|
||||||
@@ -188,7 +188,7 @@ impl ThemeColors {
|
|||||||
background: neutral().dark().step_1(),
|
background: neutral().dark().step_1(),
|
||||||
surface_background: neutral().dark().step_2(),
|
surface_background: neutral().dark().step_2(),
|
||||||
elevated_surface_background: neutral().dark().step_3(),
|
elevated_surface_background: neutral().dark().step_3(),
|
||||||
panel_background: gpui::black(),
|
panel_background: neutral().dark().step_4(),
|
||||||
overlay: neutral().dark_alpha().step_3(),
|
overlay: neutral().dark_alpha().step_3(),
|
||||||
title_bar: gpui::transparent_black(),
|
title_bar: gpui::transparent_black(),
|
||||||
title_bar_inactive: neutral().dark().step_1(),
|
title_bar_inactive: neutral().dark().step_1(),
|
||||||
@@ -246,9 +246,9 @@ impl ThemeColors {
|
|||||||
ghost_element_selected: neutral().dark().step_5(),
|
ghost_element_selected: neutral().dark().step_5(),
|
||||||
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
ghost_element_disabled: neutral().dark_alpha().step_2(),
|
||||||
|
|
||||||
tab_inactive_background: neutral().dark().step_3(),
|
tab_inactive_background: neutral().dark().step_2(),
|
||||||
tab_hover_background: neutral().dark().step_4(),
|
tab_hover_background: neutral().dark().step_3(),
|
||||||
tab_active_background: neutral().dark().step_5(),
|
tab_active_background: neutral().dark().step_4(),
|
||||||
|
|
||||||
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
|
||||||
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ use std::rc::Rc;
|
|||||||
use gpui::{px, App, Global, Pixels, SharedString, Window};
|
use gpui::{px, App, Global, Pixels, SharedString, Window};
|
||||||
|
|
||||||
mod colors;
|
mod colors;
|
||||||
|
mod platform_kind;
|
||||||
mod registry;
|
mod registry;
|
||||||
mod scale;
|
mod scale;
|
||||||
mod scrollbar_mode;
|
mod scrollbar_mode;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
pub use colors::*;
|
pub use colors::*;
|
||||||
|
pub use platform_kind::PlatformKind;
|
||||||
pub use registry::*;
|
pub use registry::*;
|
||||||
pub use scale::*;
|
pub use scale::*;
|
||||||
pub use scrollbar_mode::*;
|
pub use scrollbar_mode::*;
|
||||||
@@ -24,6 +26,9 @@ pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
|
|||||||
/// Defines window border size for platforms that use client side decorations.
|
/// Defines window border size for platforms that use client side decorations.
|
||||||
pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
|
||||||
|
|
||||||
|
/// Defines window titlebar height
|
||||||
|
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
registry::init(cx);
|
registry::init(cx);
|
||||||
|
|
||||||
@@ -70,6 +75,9 @@ pub struct Theme {
|
|||||||
|
|
||||||
/// Show the scrollbar mode, default: scrolling
|
/// Show the scrollbar mode, default: scrolling
|
||||||
pub scrollbar_mode: ScrollbarMode,
|
pub scrollbar_mode: ScrollbarMode,
|
||||||
|
|
||||||
|
/// Platform
|
||||||
|
pub platform: PlatformKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for Theme {
|
impl Deref for Theme {
|
||||||
@@ -167,7 +175,9 @@ impl Theme {
|
|||||||
|
|
||||||
impl From<ThemeFamily> for Theme {
|
impl From<ThemeFamily> for Theme {
|
||||||
fn from(family: ThemeFamily) -> Self {
|
fn from(family: ThemeFamily) -> Self {
|
||||||
|
let platform = PlatformKind::platform();
|
||||||
let mode = ThemeMode::default();
|
let mode = ThemeMode::default();
|
||||||
|
|
||||||
// Define the theme colors based on the appearance
|
// Define the theme colors based on the appearance
|
||||||
let colors = match mode {
|
let colors = match mode {
|
||||||
ThemeMode::Light => family.light(),
|
ThemeMode::Light => family.light(),
|
||||||
@@ -184,6 +194,7 @@ impl From<ThemeFamily> for Theme {
|
|||||||
mode,
|
mode,
|
||||||
colors: *colors,
|
colors: *colors,
|
||||||
theme: Rc::new(family),
|
theme: Rc::new(family),
|
||||||
|
platform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "title_bar"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
common = { path = "../common" }
|
|
||||||
theme = { path = "../theme" }
|
|
||||||
ui = { path = "../ui" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
|
||||||
smallvec.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
|
||||||
windows = { version = "0.61", features = ["Wdk_System_SystemServices"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
linicon = "2.3.0"
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
use std::mem;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use gpui::MouseButton;
|
|
||||||
use gpui::{
|
|
||||||
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
|
||||||
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window,
|
|
||||||
WindowControlArea,
|
|
||||||
};
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
|
||||||
use ui::h_flex;
|
|
||||||
|
|
||||||
use crate::platform_kind::PlatformKind;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use crate::platforms::linux::LinuxWindowControls;
|
|
||||||
use crate::platforms::windows::WindowsWindowControls;
|
|
||||||
|
|
||||||
mod platform_kind;
|
|
||||||
mod platforms;
|
|
||||||
|
|
||||||
pub struct TitleBar {
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
platform_kind: PlatformKind,
|
|
||||||
should_move: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TitleBar {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TitleBar {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
children: smallvec![],
|
|
||||||
platform_kind: PlatformKind::platform(),
|
|
||||||
should_move: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub fn height(window: &mut Window) -> Pixels {
|
|
||||||
(1.75 * window.rem_size()).max(px(36.))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub fn height(_window: &mut Window) -> Pixels {
|
|
||||||
px(32.)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
|
|
||||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
|
||||||
if window.is_window_active() && !self.should_move {
|
|
||||||
cx.theme().title_bar
|
|
||||||
} else {
|
|
||||||
cx.theme().title_bar_inactive
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cx.theme().title_bar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_children<T>(&mut self, children: T)
|
|
||||||
where
|
|
||||||
T: IntoIterator<Item = AnyElement>,
|
|
||||||
{
|
|
||||||
self.children = children.into_iter().collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for TitleBar {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for TitleBar {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let supported_controls = window.window_controls();
|
|
||||||
let decorations = window.window_decorations();
|
|
||||||
let height = Self::height(window);
|
|
||||||
let color = self.title_bar_color(window, cx);
|
|
||||||
let children = mem::take(&mut self.children);
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.window_control_area(WindowControlArea::Drag)
|
|
||||||
.w_full()
|
|
||||||
.h(height)
|
|
||||||
.map(|this| {
|
|
||||||
if window.is_fullscreen() {
|
|
||||||
this.px_2()
|
|
||||||
} else if self.platform_kind.is_mac() {
|
|
||||||
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
|
||||||
.pr_2()
|
|
||||||
.when(children.len() <= 1, |this| {
|
|
||||||
this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.px_2()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|this| match decorations {
|
|
||||||
Decorations::Server => this,
|
|
||||||
Decorations::Client { tiling, .. } => this
|
|
||||||
.when(!(tiling.top || tiling.right), |el| {
|
|
||||||
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
})
|
|
||||||
.when(!(tiling.top || tiling.left), |el| {
|
|
||||||
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.bg(color)
|
|
||||||
.content_stretch()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("title-bar")
|
|
||||||
.flex()
|
|
||||||
.flex_row()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.w_full()
|
|
||||||
.when(self.platform_kind.is_mac(), |this| {
|
|
||||||
this.on_click(|event, window, _| {
|
|
||||||
if event.click_count() == 2 {
|
|
||||||
window.titlebar_double_click();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when(self.platform_kind.is_linux(), |this| {
|
|
||||||
this.on_click(|event, window, _| {
|
|
||||||
if event.click_count() == 2 {
|
|
||||||
window.zoom_window();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.children(children),
|
|
||||||
)
|
|
||||||
.when(!window.is_fullscreen(), |this| match self.platform_kind {
|
|
||||||
PlatformKind::Linux => {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
if matches!(decorations, Decorations::Client { .. }) {
|
|
||||||
this.child(LinuxWindowControls::new())
|
|
||||||
.when(supported_controls.window_menu, |this| {
|
|
||||||
this.on_mouse_down(MouseButton::Right, move |ev, window, _| {
|
|
||||||
window.show_window_menu(ev.position)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
|
|
||||||
if this.should_move {
|
|
||||||
this.should_move = false;
|
|
||||||
window.start_window_move();
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
|
|
||||||
this.should_move = false;
|
|
||||||
}))
|
|
||||||
.on_mouse_up(
|
|
||||||
MouseButton::Left,
|
|
||||||
cx.listener(move |this, _ev, _window, _cx| {
|
|
||||||
this.should_move = false;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Left,
|
|
||||||
cx.listener(move |this, _ev, _window, _cx| {
|
|
||||||
this.should_move = true;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
this
|
|
||||||
}
|
|
||||||
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
|
||||||
PlatformKind::Mac => this,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/// Use pixels here instead of a rem-based size because the macOS traffic
|
|
||||||
/// lights are a static size, and don't scale with the rest of the UI.
|
|
||||||
///
|
|
||||||
/// Magic number: There is one extra pixel of padding on the left side due to
|
|
||||||
/// the 1px border around the window on macOS apps.
|
|
||||||
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
|
||||||
27
crates/ui/src/element_ext.rs
Normal file
27
crates/ui/src/element_ext.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use gpui::{canvas, App, Bounds, ParentElement, Pixels, Styled as _, Window};
|
||||||
|
|
||||||
|
/// A trait to extend [`gpui::Element`] with additional functionality.
|
||||||
|
pub trait ElementExt: ParentElement + Sized {
|
||||||
|
/// Add a prepaint callback to the element.
|
||||||
|
///
|
||||||
|
/// This is a helper method to get the bounds of the element after paint.
|
||||||
|
///
|
||||||
|
/// The first argument is the bounds of the element in pixels.
|
||||||
|
///
|
||||||
|
/// See also [`gpui::canvas`].
|
||||||
|
fn on_prepaint<F>(self, f: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(Bounds<Pixels>, &mut Window, &mut App) + 'static,
|
||||||
|
{
|
||||||
|
self.child(
|
||||||
|
canvas(
|
||||||
|
move |bounds, window, cx| f(bounds, window, cx),
|
||||||
|
|_, _, _, _| {},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ParentElement> ElementExt for T {}
|
||||||
@@ -37,6 +37,12 @@ pub enum IconName {
|
|||||||
Settings,
|
Settings,
|
||||||
Sun,
|
Sun,
|
||||||
Upload,
|
Upload,
|
||||||
|
PanelLeft,
|
||||||
|
PanelLeftOpen,
|
||||||
|
PanelRight,
|
||||||
|
PanelRightOpen,
|
||||||
|
PanelBottom,
|
||||||
|
PanelBottomOpen,
|
||||||
Warning,
|
Warning,
|
||||||
WindowClose,
|
WindowClose,
|
||||||
WindowMaximize,
|
WindowMaximize,
|
||||||
@@ -76,6 +82,12 @@ impl IconName {
|
|||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
Self::Sun => "icons/sun.svg",
|
Self::Sun => "icons/sun.svg",
|
||||||
Self::Upload => "icons/upload.svg",
|
Self::Upload => "icons/upload.svg",
|
||||||
|
Self::PanelLeft => "icons/panel-left.svg",
|
||||||
|
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||||
|
Self::PanelRight => "icons/panel-right.svg",
|
||||||
|
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||||
|
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||||
|
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||||
Self::Warning => "icons/warning.svg",
|
Self::Warning => "icons/warning.svg",
|
||||||
Self::WindowClose => "icons/window-close.svg",
|
Self::WindowClose => "icons/window-close.svg",
|
||||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub use element_ext::ElementExt;
|
||||||
pub use event::InteractiveElementExt;
|
pub use event::InteractiveElementExt;
|
||||||
pub use focusable::FocusableCycle;
|
pub use focusable::FocusableCycle;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
@@ -24,13 +25,12 @@ pub mod menu;
|
|||||||
pub mod modal;
|
pub mod modal;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
pub mod popover;
|
pub mod popover;
|
||||||
pub mod resizable;
|
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
pub mod skeleton;
|
pub mod skeleton;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
pub mod tab;
|
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
||||||
|
mod element_ext;
|
||||||
mod event;
|
mod event;
|
||||||
mod focusable;
|
mod focusable;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
use gpui::{Axis, Context, Window};
|
|
||||||
mod panel;
|
|
||||||
mod resize_handle;
|
|
||||||
|
|
||||||
pub use panel::*;
|
|
||||||
pub use resize_handle::*;
|
|
||||||
|
|
||||||
pub fn h_resizable(
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ResizablePanelGroup>,
|
|
||||||
) -> ResizablePanelGroup {
|
|
||||||
ResizablePanelGroup::new(window, cx).axis(Axis::Horizontal)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn v_resizable(
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<ResizablePanelGroup>,
|
|
||||||
) -> ResizablePanelGroup {
|
|
||||||
ResizablePanelGroup::new(window, cx).axis(Axis::Vertical)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resizable_panel() -> ResizablePanel {
|
|
||||||
ResizablePanel::new()
|
|
||||||
}
|
|
||||||
@@ -1,561 +0,0 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
canvas, div, px, relative, Along, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context,
|
|
||||||
Element, Entity, EntityId, EventEmitter, IntoElement, IsZero, MouseMoveEvent, MouseUpEvent,
|
|
||||||
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Style, Styled, WeakEntity,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::resize_handle;
|
|
||||||
use crate::{h_flex, v_flex, AxisExt};
|
|
||||||
|
|
||||||
pub const PANEL_MIN_SIZE: Pixels = px(100.);
|
|
||||||
|
|
||||||
pub enum ResizablePanelEvent {
|
|
||||||
Resized,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Render)]
|
|
||||||
pub struct DragPanel(pub (EntityId, usize, Axis));
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ResizablePanelGroup {
|
|
||||||
panels: Vec<Entity<ResizablePanel>>,
|
|
||||||
sizes: Vec<Pixels>,
|
|
||||||
axis: Axis,
|
|
||||||
size: Option<Pixels>,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
resizing_panel_ix: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizablePanelGroup {
|
|
||||||
pub(super) fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
axis: Axis::Horizontal,
|
|
||||||
sizes: Vec::new(),
|
|
||||||
panels: Vec::new(),
|
|
||||||
size: None,
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
resizing_panel_ix: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(&mut self, sizes: Vec<Pixels>, panels: Vec<Entity<ResizablePanel>>) {
|
|
||||||
self.sizes = sizes;
|
|
||||||
self.panels = panels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the axis of the resizable panel group, default is horizontal.
|
|
||||||
pub fn axis(mut self, axis: Axis) -> Self {
|
|
||||||
self.axis = axis;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_axis(&mut self, axis: Axis, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.axis = axis;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a resizable panel to the group.
|
|
||||||
pub fn child(
|
|
||||||
mut self,
|
|
||||||
panel: ResizablePanel,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
self.add_child(panel, window, cx);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a ResizablePanelGroup as a child to the group.
|
|
||||||
pub fn group(
|
|
||||||
self,
|
|
||||||
group: ResizablePanelGroup,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let group: ResizablePanelGroup = group;
|
|
||||||
let size = group.size;
|
|
||||||
let panel = ResizablePanel::new()
|
|
||||||
.content_view(cx.new(|_| group).into())
|
|
||||||
.when_some(size, |this, size| this.size(size));
|
|
||||||
self.child(panel, window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set size of the resizable panel group
|
|
||||||
///
|
|
||||||
/// - When the axis is horizontal, the size is the height of the group.
|
|
||||||
/// - When the axis is vertical, the size is the width of the group.
|
|
||||||
pub fn size(mut self, size: Pixels) -> Self {
|
|
||||||
self.size = Some(size);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the sum of all panel sizes within the group.
|
|
||||||
pub fn total_size(&self) -> Pixels {
|
|
||||||
self.sizes.iter().fold(px(0.0), |acc, &size| acc + size)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_child(
|
|
||||||
&mut self,
|
|
||||||
panel: ResizablePanel,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let mut panel = panel;
|
|
||||||
panel.axis = self.axis;
|
|
||||||
panel.group = Some(cx.entity().downgrade());
|
|
||||||
self.sizes.push(panel.initial_size.unwrap_or_default());
|
|
||||||
self.panels.push(cx.new(|_| panel));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_child(
|
|
||||||
&mut self,
|
|
||||||
panel: ResizablePanel,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let mut panel = panel;
|
|
||||||
panel.axis = self.axis;
|
|
||||||
panel.group = Some(cx.entity().downgrade());
|
|
||||||
|
|
||||||
self.sizes
|
|
||||||
.insert(ix, panel.initial_size.unwrap_or_default());
|
|
||||||
self.panels.insert(ix, cx.new(|_| panel));
|
|
||||||
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace a child panel with a new panel at the given index.
|
|
||||||
pub fn replace_child(
|
|
||||||
&mut self,
|
|
||||||
panel: ResizablePanel,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let mut panel = panel;
|
|
||||||
|
|
||||||
let old_panel = self.panels[ix].clone();
|
|
||||||
let old_panel_initial_size = old_panel.read(cx).initial_size;
|
|
||||||
let old_panel_size_ratio = old_panel.read(cx).size_ratio;
|
|
||||||
|
|
||||||
panel.initial_size = old_panel_initial_size;
|
|
||||||
panel.size_ratio = old_panel_size_ratio;
|
|
||||||
panel.axis = self.axis;
|
|
||||||
panel.group = Some(cx.entity().downgrade());
|
|
||||||
self.sizes[ix] = panel.initial_size.unwrap_or_default();
|
|
||||||
self.panels[ix] = cx.new(|_| panel);
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_child(&mut self, ix: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.sizes.remove(ix);
|
|
||||||
self.panels.remove(ix);
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_all_children(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.sizes.clear();
|
|
||||||
self.panels.clear();
|
|
||||||
cx.notify()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_resize_handle(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let view = cx.entity().clone();
|
|
||||||
resize_handle(("resizable-handle", ix), self.axis).on_drag(
|
|
||||||
DragPanel((cx.entity_id(), ix, self.axis)),
|
|
||||||
move |drag_panel, _, _window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
// Set current resizing panel ix
|
|
||||||
view.update(cx, |view, _| {
|
|
||||||
view.resizing_panel_ix = Some(ix);
|
|
||||||
});
|
|
||||||
cx.new(|_| drag_panel.clone())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn done_resizing(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
cx.emit(ResizablePanelEvent::Resized);
|
|
||||||
self.resizing_panel_ix = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_real_panel_sizes(&mut self, _window: &Window, cx: &App) {
|
|
||||||
for (i, panel) in self.panels.iter().enumerate() {
|
|
||||||
self.sizes[i] = panel.read(cx).bounds.size.along(self.axis)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `ix`` is the index of the panel to resize,
|
|
||||||
/// and the `size` is the new size for the panel.
|
|
||||||
fn resize_panels(
|
|
||||||
&mut self,
|
|
||||||
ix: usize,
|
|
||||||
size: Pixels,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let mut ix = ix;
|
|
||||||
// Only resize the left panels.
|
|
||||||
if ix >= self.panels.len() - 1 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let size = size.floor();
|
|
||||||
let container_size = self.bounds.size.along(self.axis);
|
|
||||||
|
|
||||||
self.sync_real_panel_sizes(window, cx);
|
|
||||||
|
|
||||||
let mut changed = size - self.sizes[ix];
|
|
||||||
let is_expand = changed > px(0.);
|
|
||||||
|
|
||||||
let main_ix = ix;
|
|
||||||
let mut new_sizes = self.sizes.clone();
|
|
||||||
|
|
||||||
if is_expand {
|
|
||||||
new_sizes[ix] = size;
|
|
||||||
|
|
||||||
// Now to expand logic is correct.
|
|
||||||
while changed > px(0.) && ix < self.panels.len() - 1 {
|
|
||||||
ix += 1;
|
|
||||||
let available_size = (new_sizes[ix] - PANEL_MIN_SIZE).max(px(0.));
|
|
||||||
let to_reduce = changed.min(available_size);
|
|
||||||
new_sizes[ix] -= to_reduce;
|
|
||||||
changed -= to_reduce;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let new_size = size.max(PANEL_MIN_SIZE);
|
|
||||||
new_sizes[ix] = new_size;
|
|
||||||
changed = size - PANEL_MIN_SIZE;
|
|
||||||
new_sizes[ix + 1] += self.sizes[ix] - new_size;
|
|
||||||
|
|
||||||
while changed < px(0.) && ix > 0 {
|
|
||||||
ix -= 1;
|
|
||||||
let available_size = self.sizes[ix] - PANEL_MIN_SIZE;
|
|
||||||
let to_increase = (changed).min(available_size);
|
|
||||||
new_sizes[ix] += to_increase;
|
|
||||||
changed += to_increase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If total size exceeds container size, adjust the main panel
|
|
||||||
let total_size: Pixels = new_sizes.iter().map(|s| s.signum()).sum::<f32>().into();
|
|
||||||
if total_size > container_size {
|
|
||||||
let overflow = total_size - container_size;
|
|
||||||
new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(PANEL_MIN_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_size = new_sizes.iter().fold(px(0.0), |acc, &size| acc + size);
|
|
||||||
self.sizes = new_sizes;
|
|
||||||
for (i, panel) in self.panels.iter().enumerate() {
|
|
||||||
let size = self.sizes[i];
|
|
||||||
if size > px(0.) {
|
|
||||||
panel.update(cx, |this, _| {
|
|
||||||
this.size = Some(size);
|
|
||||||
this.size_ratio = Some(size / total_size);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
|
|
||||||
|
|
||||||
impl Render for ResizablePanelGroup {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let view = cx.entity().clone();
|
|
||||||
let container = if self.axis.is_horizontal() {
|
|
||||||
h_flex()
|
|
||||||
} else {
|
|
||||||
v_flex()
|
|
||||||
};
|
|
||||||
|
|
||||||
container
|
|
||||||
.size_full()
|
|
||||||
.children(self.panels.iter().enumerate().map(|(ix, panel)| {
|
|
||||||
if ix > 0 {
|
|
||||||
let handle = self.render_resize_handle(ix - 1, window, cx);
|
|
||||||
panel.update(cx, |view, _| {
|
|
||||||
view.resize_handle = Some(handle.into_any_element())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
panel.clone()
|
|
||||||
}))
|
|
||||||
.child({
|
|
||||||
canvas(
|
|
||||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
|
||||||
|_, _, _, _| {},
|
|
||||||
)
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
})
|
|
||||||
.child(ResizePanelGroupElement {
|
|
||||||
view: cx.entity().clone(),
|
|
||||||
axis: self.axis,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContentBuilder = Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>;
|
|
||||||
type ContentVisible = Rc<Box<dyn Fn(&App) -> bool>>;
|
|
||||||
|
|
||||||
pub struct ResizablePanel {
|
|
||||||
group: Option<WeakEntity<ResizablePanelGroup>>,
|
|
||||||
/// Initial size is the size that the panel has when it is created.
|
|
||||||
initial_size: Option<Pixels>,
|
|
||||||
/// size is the size that the panel has when it is resized or adjusted by flex layout.
|
|
||||||
size: Option<Pixels>,
|
|
||||||
/// the size ratio that the panel has relative to its group
|
|
||||||
size_ratio: Option<f32>,
|
|
||||||
axis: Axis,
|
|
||||||
content_builder: ContentBuilder,
|
|
||||||
content_view: Option<AnyView>,
|
|
||||||
content_visible: ContentVisible,
|
|
||||||
/// The bounds of the resizable panel, when render the bounds will be updated.
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
resize_handle: Option<AnyElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizablePanel {
|
|
||||||
pub(super) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
group: None,
|
|
||||||
initial_size: None,
|
|
||||||
size: None,
|
|
||||||
size_ratio: None,
|
|
||||||
axis: Axis::Horizontal,
|
|
||||||
content_builder: None,
|
|
||||||
content_view: None,
|
|
||||||
content_visible: Rc::new(Box::new(|_| true)),
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
resize_handle: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content<F>(mut self, content: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
|
||||||
{
|
|
||||||
self.content_builder = Some(Rc::new(content));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub 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 {
|
|
||||||
self.content_view = Some(content);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the initial size of the panel.
|
|
||||||
pub fn size(mut self, size: Pixels) -> Self {
|
|
||||||
self.initial_size = Some(size);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save the real panel size, and update group sizes
|
|
||||||
fn update_size(
|
|
||||||
&mut self,
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let new_size = bounds.size.along(self.axis);
|
|
||||||
self.bounds = bounds;
|
|
||||||
self.size_ratio = None;
|
|
||||||
self.size = Some(new_size);
|
|
||||||
|
|
||||||
let entity_id = cx.entity_id();
|
|
||||||
|
|
||||||
if let Some(group) = self.group.as_ref() {
|
|
||||||
_ = group.update(cx, |view, _| {
|
|
||||||
if let Some(ix) = view.panels.iter().position(|v| v.entity_id() == entity_id) {
|
|
||||||
view.sizes[ix] = new_size;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FluentBuilder for ResizablePanel {}
|
|
||||||
|
|
||||||
impl Render for ResizablePanel {
|
|
||||||
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 total_size = self
|
|
||||||
.group
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|group| group.upgrade())
|
|
||||||
.map(|group| group.read(cx).total_size());
|
|
||||||
|
|
||||||
let view = cx.entity();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_grow()
|
|
||||||
.size_full()
|
|
||||||
.relative()
|
|
||||||
.when(self.initial_size.is_none(), |this| this.flex_shrink())
|
|
||||||
.when(self.axis.is_vertical(), |this| this.min_h(PANEL_MIN_SIZE))
|
|
||||||
.when(self.axis.is_horizontal(), |this| this.min_w(PANEL_MIN_SIZE))
|
|
||||||
.when_some(self.initial_size, |this, size| {
|
|
||||||
if size.is_zero() {
|
|
||||||
this
|
|
||||||
} else {
|
|
||||||
// The `self.size` is None, that mean the initial size for the panel, so we need set flex_shrink_0
|
|
||||||
// To let it keep the initial size.
|
|
||||||
this.when(self.size.is_none() && size > px(0.), |this| {
|
|
||||||
this.flex_shrink_0()
|
|
||||||
})
|
|
||||||
.flex_basis(size)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|this| match (self.size_ratio, self.size, total_size) {
|
|
||||||
(Some(size_ratio), _, _) => this.flex_basis(relative(size_ratio)),
|
|
||||||
(None, Some(size), Some(total_size)) => {
|
|
||||||
this.flex_basis(relative(size / total_size))
|
|
||||||
}
|
|
||||||
(None, Some(size), None) => this.flex_basis(size),
|
|
||||||
_ => this,
|
|
||||||
})
|
|
||||||
.child({
|
|
||||||
canvas(
|
|
||||||
move |bounds, window, cx| {
|
|
||||||
view.update(cx, |r, cx| r.update_size(bounds, window, cx))
|
|
||||||
},
|
|
||||||
|_, _, _, _| {},
|
|
||||||
)
|
|
||||||
.absolute()
|
|
||||||
.size_full()
|
|
||||||
})
|
|
||||||
.when_some(self.content_builder.clone(), |this, c| {
|
|
||||||
this.child(c(window, cx))
|
|
||||||
})
|
|
||||||
.when_some(self.content_view.clone(), |this, c| this.child(c))
|
|
||||||
.when_some(self.resize_handle.take(), |this, c| this.child(c))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ResizePanelGroupElement {
|
|
||||||
axis: Axis,
|
|
||||||
view: Entity<ResizablePanelGroup>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoElement for ResizePanelGroupElement {
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element for ResizePanelGroupElement {
|
|
||||||
type PrepaintState = ();
|
|
||||||
type RequestLayoutState = ();
|
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_layout(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
|
||||||
(window.request_layout(Style::default(), None, cx), ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepaint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_window: &mut Window,
|
|
||||||
_cx: &mut App,
|
|
||||||
) -> Self::PrepaintState {
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint(
|
|
||||||
&mut self,
|
|
||||||
_: Option<&gpui::GlobalElementId>,
|
|
||||||
_: Option<&gpui::InspectorElementId>,
|
|
||||||
_: Bounds<Pixels>,
|
|
||||||
_: &mut Self::RequestLayoutState,
|
|
||||||
_: &mut Self::PrepaintState,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
window.on_mouse_event({
|
|
||||||
let view = self.view.clone();
|
|
||||||
let axis = self.axis;
|
|
||||||
let current_ix = view.read(cx).resizing_panel_ix;
|
|
||||||
move |e: &MouseMoveEvent, phase, window, cx| {
|
|
||||||
if !phase.bubble() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(ix) = current_ix else { return };
|
|
||||||
|
|
||||||
view.update(cx, |view, cx| {
|
|
||||||
let panel = view
|
|
||||||
.panels
|
|
||||||
.get(ix)
|
|
||||||
.expect("BUG: invalid panel index")
|
|
||||||
.read(cx);
|
|
||||||
|
|
||||||
match axis {
|
|
||||||
Axis::Horizontal => {
|
|
||||||
view.resize_panels(ix, e.position.x - panel.bounds.left(), window, cx)
|
|
||||||
}
|
|
||||||
Axis::Vertical => {
|
|
||||||
view.resize_panels(ix, e.position.y - panel.bounds.top(), window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// When any mouse up, stop dragging
|
|
||||||
window.on_mouse_event({
|
|
||||||
let view = self.view.clone();
|
|
||||||
let current_ix = view.read(cx).resizing_panel_ix;
|
|
||||||
move |_: &MouseUpEvent, phase, window, cx| {
|
|
||||||
if current_ix.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if phase.bubble() {
|
|
||||||
view.update(cx, |view, cx| view.done_resizing(window, cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
div, px, App, Axis, Div, ElementId, InteractiveElement, IntoElement, ParentElement as _,
|
|
||||||
Pixels, RenderOnce, Stateful, StatefulInteractiveElement, Styled as _, Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::AxisExt as _;
|
|
||||||
|
|
||||||
pub const HANDLE_PADDING: Pixels = px(8.);
|
|
||||||
pub const HANDLE_SIZE: Pixels = px(2.);
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct ResizeHandle {
|
|
||||||
base: Stateful<Div>,
|
|
||||||
axis: Axis,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResizeHandle {
|
|
||||||
fn new(id: impl Into<ElementId>, axis: Axis) -> Self {
|
|
||||||
Self {
|
|
||||||
base: div().id(id.into()),
|
|
||||||
axis,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a resize handle for a resizable panel.
|
|
||||||
pub fn resize_handle(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle {
|
|
||||||
ResizeHandle::new(id, axis)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InteractiveElement for ResizeHandle {
|
|
||||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
|
||||||
self.base.interactivity()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatefulInteractiveElement for ResizeHandle {}
|
|
||||||
|
|
||||||
impl RenderOnce for ResizeHandle {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
self.base
|
|
||||||
.occlude()
|
|
||||||
.absolute()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.when(self.axis.is_horizontal(), |this| {
|
|
||||||
this.cursor_col_resize()
|
|
||||||
.top_0()
|
|
||||||
.left(px(-1.))
|
|
||||||
.w(HANDLE_SIZE)
|
|
||||||
.h_full()
|
|
||||||
.pt_12()
|
|
||||||
.pb_4()
|
|
||||||
})
|
|
||||||
.when(self.axis.is_vertical(), |this| {
|
|
||||||
this.cursor_row_resize()
|
|
||||||
.top(px(-1.))
|
|
||||||
.left_0()
|
|
||||||
.w_full()
|
|
||||||
.h(HANDLE_SIZE)
|
|
||||||
.px_6()
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.rounded_full()
|
|
||||||
.hover(|this| this.bg(cx.theme().border_variant))
|
|
||||||
.when(self.axis.is_horizontal(), |this| {
|
|
||||||
this.h_full().w(HANDLE_SIZE)
|
|
||||||
})
|
|
||||||
.when(self.axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -213,6 +213,7 @@ impl Root {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear a notification by its ID.
|
||||||
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
|
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
T: Into<SharedString>,
|
T: Into<SharedString>,
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
|
||||||
use gpui::{
|
|
||||||
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
|
||||||
RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, Window,
|
|
||||||
};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::h_flex;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct TabBar {
|
|
||||||
base: Div,
|
|
||||||
id: ElementId,
|
|
||||||
scroll_handle: ScrollHandle,
|
|
||||||
prefix: Option<AnyElement>,
|
|
||||||
suffix: Option<AnyElement>,
|
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TabBar {
|
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
||||||
Self {
|
|
||||||
base: div().px(px(-1.)),
|
|
||||||
id: id.into(),
|
|
||||||
children: SmallVec::new(),
|
|
||||||
scroll_handle: ScrollHandle::new(),
|
|
||||||
prefix: None,
|
|
||||||
suffix: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Track the scroll of the TabBar
|
|
||||||
pub fn track_scroll(mut self, scroll_handle: ScrollHandle) -> Self {
|
|
||||||
self.scroll_handle = scroll_handle;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the prefix element of the TabBar
|
|
||||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
|
||||||
self.prefix = Some(prefix.into_any_element());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the suffix element of the TabBar
|
|
||||||
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
|
|
||||||
self.suffix = Some(suffix.into_any_element());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParentElement for TabBar {
|
|
||||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
||||||
self.children.extend(elements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Styled for TabBar {
|
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
||||||
self.base.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for TabBar {
|
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
self.base
|
|
||||||
.id(self.id)
|
|
||||||
.group("tab-bar")
|
|
||||||
.relative()
|
|
||||||
.px_1()
|
|
||||||
.flex()
|
|
||||||
.flex_none()
|
|
||||||
.items_center()
|
|
||||||
.when_some(self.prefix, |this, prefix| this.child(prefix))
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.id("tabs")
|
|
||||||
.flex_grow()
|
|
||||||
.gap_1()
|
|
||||||
.overflow_x_scroll()
|
|
||||||
.track_scroll(&self.scroll_handle)
|
|
||||||
.children(self.children),
|
|
||||||
)
|
|
||||||
.when_some(self.suffix, |this, suffix| this.child(suffix))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user