update theme
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 9m53s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled

This commit is contained in:
2026-02-21 10:43:36 +07:00
parent bc588114c4
commit e3141aba19
16 changed files with 1229 additions and 1588 deletions

View File

@@ -18,15 +18,12 @@ use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, FIND_DELAY};
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::divider::Divider;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{
h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension,
};
use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
mod entry;
@@ -122,12 +119,10 @@ impl Sidebar {
}
}
InputEvent::Focus => {
this.set_input_focus(window, cx);
this.set_input_focus(true, window, cx);
this.get_contact_list(window, cx);
}
InputEvent::Blur => {
this.set_input_focus(window, cx);
}
_ => {}
};
}),
);
@@ -246,6 +241,7 @@ impl Sidebar {
}));
}
/// Set the results of the search
fn set_results(&mut self, results: Vec<PublicKey>, cx: &mut Context<Self>) {
self.find_results.update(cx, |this, cx| {
*this = Some(results);
@@ -253,6 +249,7 @@ impl Sidebar {
});
}
/// Set the finding status
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
// Disable the input to prevent duplicate requests
self.find_input.update(cx, |this, cx| {
@@ -264,13 +261,14 @@ impl Sidebar {
cx.notify();
}
fn set_input_focus(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.find_focused = !self.find_focused;
/// Set the focus status of the input element.
fn set_input_focus(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
self.find_focused = status;
cx.notify();
// Reset the find panel
if !self.find_focused {
self.reset(window, cx);
// Focus to the input element
if !status {
window.focus_prev(cx);
}
}
@@ -356,7 +354,8 @@ impl Sidebar {
}
/// Set the active filter for the sidebar.
fn set_filter(&mut self, kind: RoomKind, cx: &mut Context<Self>) {
fn set_filter(&mut self, kind: RoomKind, window: &mut Window, cx: &mut Context<Self>) {
self.set_input_focus(false, window, cx);
self.filter.update(cx, |this, cx| {
*this = kind;
cx.notify();
@@ -495,12 +494,13 @@ impl Render for Sidebar {
v_flex()
.image_cache(self.image_cache.clone())
.size_full()
.relative()
.gap_2()
.child(
h_flex()
.h(TITLEBAR_HEIGHT)
.border_b_1()
.border_color(cx.theme().border)
.border_color(cx.theme().border_variant)
.bg(cx.theme().elevated_surface_background)
.child(
TextInput::new(&self.find_input)
.appearance(false)
@@ -520,22 +520,17 @@ impl Render for Sidebar {
)
.child(
h_flex()
.h(TITLEBAR_HEIGHT)
.px_2()
.gap_2()
.justify_center()
.border_b_1()
.border_color(cx.theme().border)
.when(show_find_panel, |this| {
this.child(
Button::new("search-results")
.icon(IconName::Search)
.label("Search")
.tooltip("All search results")
.small()
.underline()
.ghost()
.ghost_alt()
.font_semibold()
.rounded_none()
.h_full()
.flex_1()
.selected(true),
)
@@ -552,21 +547,16 @@ impl Render for Sidebar {
.when(!show_find_panel, |this| this.label("Inbox"))
.tooltip("All ongoing conversations")
.small()
.underline()
.ghost()
.ghost_alt()
.font_semibold()
.rounded_none()
.h_full()
.flex_1()
.disabled(show_find_panel)
.selected(
!show_find_panel && self.current_filter(&RoomKind::Ongoing, cx),
)
.on_click(cx.listener(|this, _ev, _window, cx| {
this.set_filter(RoomKind::Ongoing, cx);
.on_click(cx.listener(|this, _ev, window, cx| {
this.set_filter(RoomKind::Ongoing, window, cx);
})),
)
.child(Divider::vertical())
.child(
Button::new("requests")
.map(|this| {
@@ -579,31 +569,26 @@ impl Render for Sidebar {
.when(!show_find_panel, |this| this.label("Requests"))
.tooltip("Incoming new conversations")
.small()
.ghost()
.underline()
.ghost_alt()
.font_semibold()
.rounded_none()
.h_full()
.flex_1()
.disabled(show_find_panel)
.selected(
!show_find_panel && !self.current_filter(&RoomKind::Ongoing, cx),
)
.when(self.new_requests, |this| {
this.child(div().size_1().rounded_full().bg(cx.theme().cursor))
})
.on_click(cx.listener(|this, _ev, _window, cx| {
this.set_filter(RoomKind::default(), cx);
.on_click(cx.listener(|this, _ev, window, cx| {
this.set_filter(RoomKind::default(), window, cx);
})),
),
)
.when(!show_find_panel && !loading && total_rooms == 0, |this| {
this.child(
div().mt_2().px_2().child(
div().px_2().child(
v_flex()
.p_3()
.h_24()
.w_full()
.border_2()
.border_dashed()
.border_color(cx.theme().border_variant)
@@ -629,9 +614,8 @@ impl Render for Sidebar {
v_flex()
.h_full()
.px_1p5()
.mt_2()
.flex_1()
.gap_1()
.flex_1()
.overflow_y_hidden()
.when(show_find_panel, |this| {
this.gap_3()
@@ -747,7 +731,7 @@ impl Render for Sidebar {
.bg(cx.theme().background.opacity(0.85))
.border_color(cx.theme().border_disabled)
.border_1()
.when(cx.theme().shadow, |this| this.shadow_sm())
.when(cx.theme().shadow, |this| this.shadow_xs())
.rounded_full()
.text_xs()
.font_semibold()

View File

@@ -9,7 +9,7 @@ use gpui::{
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, RelayState};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use title_bar::TitleBar;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -41,10 +41,17 @@ impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
let titlebar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx).panel_style(PanelStyle::TabBar));
let dock = cx.new(|cx| DockArea::new(window, cx).style(PanelStyle::TabBar));
let mut subscriptions = smallvec![];
subscriptions.push(
// Observe system appearance and update theme
cx.observe_window_appearance(window, |_this, window, cx| {
Theme::sync_system_appearance(Some(window), cx);
}),
);
subscriptions.push(
// Observe all events emitted by the chat registry
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
@@ -236,11 +243,11 @@ impl Workspace {
h_flex()
.h_6()
.w_full()
.px_1()
.px_2()
.text_xs()
.text_color(cx.theme().warning_foreground)
.bg(cx.theme().warning_background)
.rounded_sm()
.rounded_full()
.child(SharedString::from(
"User hasn't configured a messaging relay list",
)),

View File

@@ -78,8 +78,10 @@ pub struct ThemeColors {
// Tab colors
pub tab_inactive_background: Hsla,
pub tab_hover_background: Hsla,
pub tab_inactive_foreground: Hsla,
pub tab_active_background: Hsla,
pub tab_active_foreground: Hsla,
pub tab_hover_foreground: Hsla,
// Scrollbar colors
pub scrollbar_thumb_background: Hsla,
@@ -106,10 +108,10 @@ impl ThemeColors {
background: neutral().light().step_1(),
surface_background: neutral().light().step_2(),
elevated_surface_background: neutral().light().step_3(),
panel_background: gpui::white(),
panel_background: neutral().light().step_1(),
overlay: neutral().light_alpha().step_3(),
title_bar: gpui::transparent_black(),
title_bar_inactive: neutral().light().step_1(),
title_bar: neutral().light().step_2(),
title_bar_inactive: neutral().light().step_3(),
window_border: hsl(240.0, 5.9, 78.0),
border: neutral().light().step_6(),
@@ -164,9 +166,11 @@ impl ThemeColors {
ghost_element_selected: neutral().light().step_5(),
ghost_element_disabled: neutral().light_alpha().step_2(),
tab_inactive_background: neutral().light().step_3(),
tab_hover_background: neutral().light().step_4(),
tab_active_background: neutral().light().step_5(),
tab_inactive_background: neutral().light().step_2(),
tab_inactive_foreground: neutral().light().step_11(),
tab_active_background: neutral().light().step_1(),
tab_active_foreground: neutral().light().step_12(),
tab_hover_foreground: brand().light().step_9(),
scrollbar_thumb_background: neutral().light_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().light_alpha().step_4(),
@@ -246,9 +250,11 @@ impl ThemeColors {
ghost_element_selected: neutral().dark().step_5(),
ghost_element_disabled: neutral().dark_alpha().step_2(),
tab_inactive_background: neutral().dark().step_3(),
tab_hover_background: neutral().dark().step_4(),
tab_active_background: neutral().dark().step_5(),
tab_inactive_background: neutral().dark().step_2(),
tab_inactive_foreground: neutral().dark().step_11(),
tab_active_background: neutral().dark().step_3(),
tab_active_foreground: neutral().dark().step_12(),
tab_hover_foreground: brand().dark().step_9(),
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),

View File

@@ -106,7 +106,7 @@ impl Render for TitleBar {
})
.bg(color)
.border_b_1()
.border_color(cx.theme().border)
.border_color(cx.theme().border_variant)
.content_stretch()
.child(
h_flex()

View File

@@ -1,32 +1,27 @@
use std::ops::Deref;
use std::sync::Arc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, App, AppContext, Axis, Context, Element, Entity, InteractiveElement as _, IntoElement,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window,
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
Window,
};
use serde::{Deserialize, Serialize};
use theme::ActiveTheme;
use super::{DockArea, DockItem};
use crate::dock_area::panel::PanelView;
use crate::dock_area::tab_panel::TabPanel;
use crate::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE};
use crate::{AxisExt as _, StyledExt};
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
use crate::StyledExt;
#[derive(Clone, Render)]
struct ResizePanel;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockPlacement {
#[serde(rename = "center")]
Center,
#[serde(rename = "left")]
Left,
#[serde(rename = "bottom")]
Bottom,
#[serde(rename = "right")]
Right,
}
@@ -58,16 +53,21 @@ impl DockPlacement {
pub struct Dock {
pub(super) placement: DockPlacement,
dock_area: WeakEntity<DockArea>,
/// Dock layout
pub(crate) panel: DockItem,
/// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height.
pub(super) size: Pixels,
/// Whether the Dock is open
pub(super) open: bool,
/// Whether the Dock is collapsible, default: true
pub(super) collapsible: bool,
// Runtime state
/// Whether the Dock is resizing
is_resizing: bool,
resizing: bool,
}
impl Dock {
@@ -98,7 +98,7 @@ impl Dock {
open: true,
collapsible: true,
size: px(200.0),
is_resizing: false,
resizing: false,
}
}
@@ -231,54 +231,16 @@ impl Dock {
cx: &mut Context<Self>,
) -> impl IntoElement {
let axis = self.placement.axis();
let neg_offset = -HANDLE_PADDING;
let view = cx.entity().clone();
div()
.id("resize-handle")
.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)),
)
resize_handle("resize-handle", axis)
.placement(self.placement)
.on_drag(ResizePanel {}, move |info, _, _, cx| {
cx.stop_propagation();
view.update(cx, |view, _| {
view.is_resizing = true;
view.update(cx, |view, _cx| {
view.resizing = true;
});
cx.new(|_| info.clone())
cx.new(|_| info.deref().clone())
})
}
@@ -288,7 +250,7 @@ impl Dock {
_window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.is_resizing {
if !self.resizing {
return;
}
@@ -349,7 +311,7 @@ impl Dock {
}
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({
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| {
if !is_resizing {
return;

View File

@@ -2,30 +2,23 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
actions, canvas, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges,
Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity,
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
};
use crate::dock_area::dock::{Dock, DockPlacement};
use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView};
use crate::dock_area::stack_panel::StackPanel;
use crate::dock_area::tab_panel::TabPanel;
use crate::ElementExt;
pub mod dock;
pub mod panel;
pub mod stack_panel;
pub mod tab_panel;
actions!(
dock,
[
/// Zoom the current panel
ToggleZoom,
/// Close the current panel
ClosePanel
]
);
actions!(dock, [ToggleZoom, ClosePanel]);
pub enum DockEvent {
/// The layout of the dock has changed, subscribers this to save the layout.
@@ -38,20 +31,31 @@ pub enum DockEvent {
/// The main area of the dock.
pub struct DockArea {
pub(crate) bounds: Bounds<Pixels>,
/// The center view of the dockarea.
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.
left_dock: Option<Entity<Dock>>,
/// The bottom dock of the dock_area.
bottom_dock: Option<Entity<Dock>>,
/// The right dock of the dock_area.
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.
zoom_view: Option<AnyView>,
/// Lock panels layout, but allow to resize.
is_locked: bool,
/// The panel style, default is [`PanelStyle::Default`](PanelStyle::Default).
pub(crate) panel_style: PanelStyle,
subscriptions: Vec<Subscription>,
@@ -330,6 +334,7 @@ impl DockArea {
items: dock_item,
zoom_view: None,
toggle_button_panels: Edges::default(),
toggle_button_visible: true,
left_dock: None,
right_dock: None,
bottom_dock: None,
@@ -344,7 +349,7 @@ impl DockArea {
}
/// Set the panel style of the dock area.
pub fn panel_style(mut self, style: PanelStyle) -> Self {
pub fn style(mut self, style: PanelStyle) -> Self {
self.panel_style = style;
self
}
@@ -649,31 +654,35 @@ impl DockArea {
cx.subscribe_in(
view,
window,
move |_, panel, event, window, cx| match event {
move |_this, panel, event, window, cx| match event {
PanelEvent::ZoomIn => {
let panel = panel.clone();
cx.spawn_in(window, async move |view, window| {
_ = view.update_in(window, |view, window, cx| {
view.update_in(window, |view, window, cx| {
view.set_zoomed_in(panel, window, cx);
cx.notify();
});
})
.ok();
})
.detach();
}
PanelEvent::ZoomOut => cx
.spawn_in(window, async move |view, window| {
PanelEvent::ZoomOut => {
cx.spawn_in(window, async move |view, window| {
_ = view.update_in(window, |view, window, cx| {
view.set_zoomed_out(window, cx);
});
})
.detach(),
.detach();
}
PanelEvent::LayoutChanged => {
cx.spawn_in(window, async move |view, window| {
_ = view.update_in(window, |view, window, cx| {
view.update_in(window, |view, window, cx| {
view.update_toggle_button_tab_panels(window, cx)
});
})
.ok();
})
.detach();
// Emit layout changed event for dock
cx.emit(DockEvent::LayoutChanged);
}
},
@@ -746,14 +755,7 @@ impl Render for DockArea {
.relative()
.size_full()
.overflow_hidden()
.child(
canvas(
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _, _| {},
)
.absolute()
.size_full(),
)
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds))
.map(|this| {
if let Some(zoom_view) = self.zoom_view.clone() {
this.child(zoom_view)

View File

@@ -6,6 +6,7 @@ use gpui::{
use crate::button::Button;
use crate::menu::PopupMenu;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PanelEvent {
ZoomIn,
ZoomOut,

View File

@@ -7,13 +7,14 @@ use gpui::{
Window,
};
use smallvec::SmallVec;
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
use super::{DockArea, PanelEvent};
use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::tab_panel::TabPanel;
use crate::resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
ResizablePanelGroup,
resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,
PANEL_MIN_SIZE,
};
use crate::{h_flex, AxisExt as _, Placement};
@@ -22,9 +23,8 @@ pub struct StackPanel {
pub(super) axis: Axis,
focus_handle: FocusHandle,
pub(crate) panels: SmallVec<[Arc<dyn PanelView>; 2]>,
panel_group: Entity<ResizablePanelGroup>,
#[allow(dead_code)]
subscriptions: Vec<Subscription>,
state: Entity<ResizableState>,
_subscriptions: Vec<Subscription>,
}
impl Panel for StackPanel {
@@ -39,28 +39,23 @@ impl Panel for StackPanel {
impl StackPanel {
pub fn new(axis: Axis, window: &mut Window, cx: &mut Context<Self>) -> Self {
let panel_group = cx.new(|cx| {
if axis == Axis::Horizontal {
h_resizable(window, cx)
} else {
v_resizable(window, cx)
}
});
let state = cx.new(|_| ResizableState::default());
// Bubble up the resize event.
let subscriptions = vec![cx.subscribe_in(
&panel_group,
window,
|_, _, _: &ResizablePanelEvent, _, cx| cx.emit(PanelEvent::LayoutChanged),
)];
let subscriptions =
vec![
cx.subscribe_in(&state, window, |_, _, _: &ResizablePanelEvent, _, cx| {
cx.emit(PanelEvent::LayoutChanged)
}),
];
Self {
axis,
parent: None,
focus_handle: cx.focus_handle(),
panels: SmallVec::new(),
panel_group,
subscriptions,
state,
_subscriptions: subscriptions,
}
}
@@ -70,7 +65,7 @@ impl StackPanel {
}
/// Return true if self or parent only have last panel.
pub(super) fn is_last_panel(&self, cx: &App) -> bool {
pub fn is_last_panel(&self, cx: &App) -> bool {
if self.panels.len() > 1 {
return false;
}
@@ -84,12 +79,12 @@ impl StackPanel {
true
}
pub(super) fn panels_len(&self) -> usize {
pub fn panels_len(&self) -> usize {
self.panels.len()
}
/// Return the index of the panel.
pub(crate) fn index_of_panel(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
pub fn index_of_panel(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
self.panels.iter().position(|p| p == &panel)
}
@@ -172,13 +167,6 @@ impl StackPanel {
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(
&mut self,
panel: Arc<dyn PanelView>,
@@ -225,14 +213,21 @@ impl StackPanel {
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.panel_group.update(cx, |view, cx| {
view.insert_child(
Self::new_resizable_panel(panel.clone(), size),
ix,
window,
cx,
)
// Update resizable state
self.state.update(cx, |state, cx| {
state.insert_panel(Some(size), Some(ix), cx);
});
cx.emit(PanelEvent::LayoutChanged);
@@ -240,47 +235,47 @@ impl StackPanel {
}
/// Remove panel from the stack.
///
/// If `ix` is not found, do nothing.
pub fn remove_panel(
&mut self,
panel: Arc<dyn PanelView>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(ix) = self.index_of_panel(panel.clone()) {
self.panels.remove(ix);
self.panel_group.update(cx, |view, cx| {
view.remove_child(ix, window, cx);
});
let Some(ix) = self.index_of_panel(panel.clone()) else {
return;
};
cx.emit(PanelEvent::LayoutChanged);
self.remove_self_if_empty(window, cx);
}
self.panels.remove(ix);
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.
pub(super) fn replace_panel(
pub fn replace_panel(
&mut self,
old_panel: Arc<dyn PanelView>,
new_panel: Entity<StackPanel>,
window: &mut Window,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(ix) = self.index_of_panel(old_panel.clone()) {
self.panels[ix] = Arc::new(new_panel.clone());
self.panel_group.update(cx, |view, cx| {
view.replace_child(
Self::new_resizable_panel(Arc::new(new_panel.clone()), None),
ix,
window,
cx,
);
self.state.update(cx, |state, cx| {
state.replace_panel(ix, ResizablePanelState::default(), cx);
});
cx.emit(PanelEvent::LayoutChanged);
}
}
/// If children is empty, remove self from parent view.
pub(crate) fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub fn remove_self_if_empty(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.is_root() {
return;
}
@@ -301,11 +296,7 @@ impl StackPanel {
}
/// Find the first top left in the stack.
pub(super) fn left_top_tab_panel(
&self,
check_parent: bool,
cx: &App,
) -> Option<Entity<TabPanel>> {
pub fn left_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
if check_parent {
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
if let Some(panel) = parent.read(cx).left_top_tab_panel(true, cx) {
@@ -329,11 +320,7 @@ impl StackPanel {
}
/// Find the first top right in the stack.
pub(super) fn right_top_tab_panel(
&self,
check_parent: bool,
cx: &App,
) -> Option<Entity<TabPanel>> {
pub fn right_top_tab_panel(&self, check_parent: bool, cx: &App) -> Option<Entity<TabPanel>> {
if check_parent {
if let Some(parent) = self.parent.as_ref().and_then(|parent| parent.upgrade()) {
if let Some(panel) = parent.read(cx).right_top_tab_panel(true, cx) {
@@ -362,17 +349,17 @@ impl StackPanel {
}
/// Remove all panels from the stack.
pub(super) fn remove_all_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context<Self>) {
self.panels.clear();
self.panel_group
.update(cx, |view, cx| view.remove_all_children(window, cx));
self.state.update(cx, |state, cx| {
state.clear();
cx.notify();
});
}
/// Change the axis of the stack panel.
pub(super) fn set_axis(&mut self, axis: Axis, window: &mut Window, cx: &mut Context<Self>) {
pub fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context<Self>) {
self.axis = axis;
self.panel_group
.update(cx, |view, cx| view.set_axis(axis, window, cx));
cx.notify();
}
}
@@ -388,10 +375,23 @@ impl EventEmitter<PanelEvent> for StackPanel {}
impl EventEmitter<DismissEvent> 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()
.size_full()
.overflow_hidden()
.child(self.panel_group.clone())
.bg(cx.theme().panel_background)
.when(cx.theme().platform.is_linux(), |this| {
this.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.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))
})),
)
}
}

View File

@@ -7,13 +7,13 @@ use gpui::{
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
use super::panel::PanelView;
use super::stack_panel::StackPanel;
use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::button::{Button, ButtonVariants as _};
use crate::dock_area::panel::Panel;
use crate::dock_area::dock::DockPlacement;
use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::stack_panel::StackPanel;
use crate::dock_area::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::menu::{DropdownMenu, PopupMenu};
use crate::tab::tab_bar::TabBar;
use crate::tab::Tab;
@@ -65,16 +65,29 @@ impl Render for DragPanel {
pub struct TabPanel {
focus_handle: FocusHandle,
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>>,
/// Current active panel index
pub(crate) active_ix: usize,
/// If this is true, the Panel closeable will follow the active panel's closeable,
/// otherwise this TabPanel will not able to close
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,
is_zoomed: bool,
is_collapsed: bool,
/// Whether the tab panel is zoomeds
zoomed: bool,
/// Whether the tab panel is collapsed
collapsed: bool,
/// When drag move, will get the placement of the panel to be split
will_split_placement: Option<Placement>,
}
@@ -142,8 +155,8 @@ impl TabPanel {
active_ix: 0,
tab_bar_scroll_handle: ScrollHandle::new(),
will_split_placement: None,
is_zoomed: false,
is_collapsed: false,
zoomed: false,
collapsed: false,
closable: true,
}
}
@@ -339,7 +352,7 @@ impl TabPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.is_collapsed = collapsed;
self.collapsed = collapsed;
cx.notify();
}
@@ -352,7 +365,7 @@ impl TabPanel {
return true;
}
if self.is_zoomed {
if self.zoomed {
return true;
}
@@ -408,7 +421,7 @@ impl TabPanel {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let is_zoomed = self.is_zoomed && state.zoomable;
let is_zoomed = self.zoomed && state.zoomable;
let view = cx.entity().clone();
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
let toolbar = self.toolbar_buttons(window, cx);
@@ -420,7 +433,7 @@ impl TabPanel {
.occlude()
.rounded_full()
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
.when(self.is_zoomed, |this| {
.when(self.zoomed, |this| {
this.child(
Button::new("zoom")
.icon(IconName::Zoom)
@@ -433,8 +446,7 @@ impl TabPanel {
)
})
.when(has_toolbar, |this| {
this.bg(cx.theme().surface_background)
.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
})
.child(
Button::new("menu")
@@ -461,21 +473,113 @@ 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(
&self,
state: &TabState,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let view = cx.entity().clone();
// Get the dock area entity
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();
};
let panel_style = dock_area.read(cx).panel_style;
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();
if !panel.visible(cx) {
@@ -488,7 +592,20 @@ impl TabPanel {
.line_height(rems(1.0))
.h(px(30.))
.py_2()
.px_3()
.pl_3()
.pr_2()
.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(
div()
.id("tab")
@@ -507,7 +624,7 @@ impl TabPanel {
this.on_drag(
DragPanel {
panel: panel.clone(),
tab_panel: view,
tab_panel: cx.entity(),
},
|drag, _, _, cx| {
cx.stop_propagation();
@@ -526,25 +643,43 @@ impl TabPanel {
.into_any_element();
}
let tabs_count = self.panels.len();
TabBar::new("tab-bar")
.track_scroll(self.tab_bar_scroll_handle.clone())
TabBar::new()
.track_scroll(&self.tab_bar_scroll_handle)
.h(TITLEBAR_HEIGHT)
.when(has_extend_dock_button, |this| {
this.prefix(
h_flex()
.items_center()
.top_0()
.right(-px(1.))
.border_r_1()
.border_b_1()
.h_full()
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.px_2()
.children(left_dock_button)
.children(bottom_dock_button),
)
})
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
let disabled = self.collapsed;
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) {
return None;
}
// Always not show active tab style, if the panel is collapsed
if self.is_collapsed {
if self.collapsed {
active = false;
}
Some(
Tab::new(("tab", ix), panel.title(cx))
Tab::new()
.ix(ix)
.label(panel.title(cx))
.py_2()
.selected(active)
.disabled(disabled)
@@ -563,7 +698,7 @@ impl TabPanel {
}))
.when(state.draggable, |this| {
this.on_drag(
DragPanel::new(panel.clone(), view.clone()),
DragPanel::new(panel.clone(), cx.entity().clone()),
|drag, _, _, cx| {
cx.stop_propagation();
cx.new(|_| drag.clone())
@@ -587,16 +722,17 @@ impl TabPanel {
}),
)
}))
.child(
.last_empty_space(
// empty space to allow move to last tab right
div()
.id("tab-bar-empty-space")
.h_full()
.flex_grow()
.min_w_16()
.rounded(cx.theme().radius)
.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)
})
.on_drop(cx.listener(
@@ -614,16 +750,22 @@ impl TabPanel {
))
}),
)
.suffix(
h_flex()
.items_center()
.top_0()
.right_0()
.h_full()
.px_2()
.gap_1()
.child(self.render_toolbar(state, window, cx)),
)
.when(!self.collapsed, |this| {
this.suffix(
h_flex()
.items_center()
.px_2()
.gap_1()
.top_0()
.right_0()
.h_full()
.border_color(cx.theme().border)
.border_l_1()
.border_b_1()
.child(self.render_toolbar(state, window, cx))
.when_some(right_dock_button, |this, btn| this.child(btn)),
)
})
.into_any_element()
}
@@ -633,7 +775,7 @@ impl TabPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
if self.is_collapsed {
if self.collapsed {
return Empty {}.into_any_element();
}
@@ -646,14 +788,13 @@ impl TabPanel {
.group("")
.overflow_hidden()
.flex_1()
.p_1()
.child(
div()
.size_full()
.rounded(cx.theme().radius_lg)
.when(cx.theme().shadow, |this| this.shadow_sm())
.when(cx.theme().mode.is_dark(), |this| this.shadow_lg())
.bg(cx.theme().panel_background)
.when(cx.theme().platform.is_linux(), |this| {
this.rounded_b(CLIENT_SIDE_DECORATION_ROUNDING)
})
.overflow_hidden()
.child(
active_panel
@@ -667,7 +808,6 @@ impl TabPanel {
div()
.invisible()
.absolute()
.p_1()
.child(
div()
.rounded(cx.theme().radius_lg)
@@ -911,16 +1051,16 @@ impl TabPanel {
return;
}
if !self.is_zoomed {
if !self.zoomed {
cx.emit(PanelEvent::ZoomIn)
} else {
cx.emit(PanelEvent::ZoomOut)
}
self.is_zoomed = !self.is_zoomed;
self.zoomed = !self.zoomed;
cx.spawn({
let is_zoomed = self.is_zoomed;
let is_zoomed = self.zoomed;
async move |view, cx| {
view.update(cx, |view, cx| {
view.set_zoomed(is_zoomed, cx);
@@ -933,7 +1073,7 @@ impl TabPanel {
fn on_action_close_panel(
&mut self,
_: &ClosePanel,
_ev: &ClosePanel,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -954,7 +1094,6 @@ impl Focusable for TabPanel {
}
impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel {
@@ -975,11 +1114,12 @@ impl Render for TabPanel {
}
v_flex()
.when(!self.is_collapsed, |this| {
.when(!self.collapsed, |this| {
this.on_action(cx.listener(Self::on_action_toggle_zoom))
.on_action(cx.listener(Self::on_action_close_panel))
})
.id("tab-panel")
.tab_group()
.track_focus(&focus_handle)
.size_full()
.overflow_hidden()

View File

@@ -1,811 +0,0 @@
use gpui::prelude::FluentBuilder;
use gpui::{
anchored, canvas, deferred, div, px, rems, AnyElement, App, AppContext, Bounds, ClickEvent,
Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
};
use theme::ActiveTheme;
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
use crate::input::clear_button::clear_button;
use crate::list::{List, ListDelegate, ListItem};
use crate::{h_flex, v_flex, Disableable as _, Icon, IconName, Sizable, Size, StyleSized};
const CONTEXT: &str = "Dropdown";
#[derive(Clone)]
pub enum ListEvent {
/// Single click or move to selected row.
SelectItem(usize),
/// Double click on the row.
ConfirmItem(usize),
// Cancel the selection.
Cancel,
}
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("up", SelectUp, Some(CONTEXT)),
KeyBinding::new("down", SelectDown, Some(CONTEXT)),
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
KeyBinding::new(
"secondary-enter",
Confirm { secondary: true },
Some(CONTEXT),
),
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
])
}
/// A trait for items that can be displayed in a dropdown.
pub trait DropdownItem {
type Value: Clone;
fn title(&self) -> SharedString;
/// Customize the display title used to selected item in Dropdown Input.
///
/// If return None, the title will be used.
fn display_title(&self) -> Option<AnyElement> {
None
}
fn value(&self) -> &Self::Value;
}
impl DropdownItem for String {
type Value = Self;
fn title(&self) -> SharedString {
SharedString::from(self.to_string())
}
fn value(&self) -> &Self::Value {
self
}
}
impl DropdownItem for SharedString {
type Value = Self;
fn title(&self) -> SharedString {
SharedString::from(self.to_string())
}
fn value(&self) -> &Self::Value {
self
}
}
pub trait DropdownDelegate: Sized {
type Item: DropdownItem;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn get(&self, ix: usize) -> Option<&Self::Item>;
fn position<V>(&self, value: &V) -> Option<usize>
where
Self::Item: DropdownItem<Value = V>,
V: PartialEq,
{
(0..self.len()).find(|&i| self.get(i).is_some_and(|item| item.value() == value))
}
fn can_search(&self) -> bool {
false
}
fn perform_search(&mut self, _query: &str, _window: &mut Window, _: &mut App) -> Task<()> {
Task::ready(())
}
}
impl<T: DropdownItem> DropdownDelegate for Vec<T> {
type Item = T;
fn len(&self) -> usize {
self.len()
}
fn get(&self, ix: usize) -> Option<&Self::Item> {
self.as_slice().get(ix)
}
fn position<V>(&self, value: &V) -> Option<usize>
where
Self::Item: DropdownItem<Value = V>,
V: PartialEq,
{
self.iter().position(|v| v.value() == value)
}
}
struct DropdownListDelegate<D: DropdownDelegate + 'static> {
delegate: D,
dropdown: WeakEntity<DropdownState<D>>,
selected_index: Option<usize>,
}
impl<D> ListDelegate for DropdownListDelegate<D>
where
D: DropdownDelegate + 'static,
{
type Item = ListItem;
fn items_count(&self, _: &App) -> usize {
self.delegate.len()
}
fn render_item(
&self,
ix: usize,
_: &mut gpui::Window,
cx: &mut gpui::Context<List<Self>>,
) -> Option<Self::Item> {
let selected = self.selected_index == Some(ix);
let size = self
.dropdown
.upgrade()
.map_or(Size::Medium, |dropdown| dropdown.read(cx).size);
if let Some(item) = self.delegate.get(ix) {
let list_item = ListItem::new(("list-item", ix))
.check_icon(IconName::Check)
.selected(selected)
.input_font_size(size)
.list_size(size)
.child(div().whitespace_nowrap().child(item.title().to_string()));
Some(list_item)
} else {
None
}
}
fn cancel(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {
let dropdown = self.dropdown.clone();
cx.defer_in(window, move |_, window, cx| {
_ = dropdown.update(cx, |this, cx| {
this.open = false;
this.focus(window, cx);
});
});
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
let selected_value = self
.selected_index
.and_then(|ix| self.delegate.get(ix))
.map(|item| item.value().clone());
let dropdown = self.dropdown.clone();
cx.defer_in(window, move |_, window, cx| {
_ = dropdown.update(cx, |this, cx| {
cx.emit(DropdownEvent::Confirm(selected_value.clone()));
this.selected_value = selected_value;
this.open = false;
this.focus(window, cx);
});
});
}
fn perform_search(
&mut self,
query: &str,
window: &mut Window,
cx: &mut Context<List<Self>>,
) -> Task<()> {
self.dropdown.upgrade().map_or(Task::ready(()), |dropdown| {
dropdown.update(cx, |_, cx| self.delegate.perform_search(query, window, cx))
})
}
fn set_selected_index(
&mut self,
ix: Option<usize>,
_: &mut Window,
_: &mut Context<List<Self>>,
) {
self.selected_index = ix;
}
fn render_empty(&self, window: &mut Window, cx: &mut Context<List<Self>>) -> impl IntoElement {
if let Some(empty) = self
.dropdown
.upgrade()
.and_then(|dropdown| dropdown.read(cx).empty.as_ref())
{
empty(window, cx).into_any_element()
} else {
h_flex()
.justify_center()
.py_6()
.text_color(cx.theme().text_muted)
.child(Icon::new(IconName::Loader).size(px(28.)))
.into_any_element()
}
}
}
pub enum DropdownEvent<D: DropdownDelegate + 'static> {
Confirm(Option<<D::Item as DropdownItem>::Value>),
}
type DropdownStateEmpty = Option<Box<dyn Fn(&Window, &App) -> AnyElement>>;
/// State of the [`Dropdown`].
pub struct DropdownState<D: DropdownDelegate + 'static> {
focus_handle: FocusHandle,
list: Entity<List<DropdownListDelegate<D>>>,
size: Size,
empty: DropdownStateEmpty,
/// Store the bounds of the input
bounds: Bounds<Pixels>,
open: bool,
selected_value: Option<<D::Item as DropdownItem>::Value>,
_subscriptions: Vec<Subscription>,
}
/// A Dropdown element.
#[derive(IntoElement)]
pub struct Dropdown<D: DropdownDelegate + 'static> {
id: ElementId,
state: Entity<DropdownState<D>>,
size: Size,
icon: Option<Icon>,
cleanable: bool,
placeholder: Option<SharedString>,
title_prefix: Option<SharedString>,
empty: Option<AnyElement>,
width: Length,
menu_width: Length,
disabled: bool,
}
pub struct SearchableVec<T> {
items: Vec<T>,
matched_items: Vec<T>,
}
impl<T: DropdownItem + Clone> SearchableVec<T> {
pub fn new(items: impl Into<Vec<T>>) -> Self {
let items = items.into();
Self {
items: items.clone(),
matched_items: items,
}
}
}
impl<T: DropdownItem + Clone> DropdownDelegate for SearchableVec<T> {
type Item = T;
fn len(&self) -> usize {
self.matched_items.len()
}
fn get(&self, ix: usize) -> Option<&Self::Item> {
self.matched_items.get(ix)
}
fn position<V>(&self, value: &V) -> Option<usize>
where
Self::Item: DropdownItem<Value = V>,
V: PartialEq,
{
for (ix, item) in self.matched_items.iter().enumerate() {
if item.value() == value {
return Some(ix);
}
}
None
}
fn can_search(&self) -> bool {
true
}
fn perform_search(&mut self, query: &str, _window: &mut Window, _: &mut App) -> Task<()> {
self.matched_items = self
.items
.iter()
.filter(|item| item.title().to_lowercase().contains(&query.to_lowercase()))
.cloned()
.collect();
Task::ready(())
}
}
impl From<Vec<SharedString>> for SearchableVec<SharedString> {
fn from(items: Vec<SharedString>) -> Self {
Self {
items: items.clone(),
matched_items: items,
}
}
}
impl<D> DropdownState<D>
where
D: DropdownDelegate + 'static,
{
pub fn new(
delegate: D,
selected_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let delegate = DropdownListDelegate {
delegate,
dropdown: cx.entity().downgrade(),
selected_index,
};
let searchable = delegate.delegate.can_search();
let list = cx.new(|cx| {
let mut list = List::new(delegate, window, cx)
.max_h(rems(20.))
.reset_on_cancel(false);
if !searchable {
list = list.no_query();
}
list
});
let _subscriptions = vec![
cx.on_blur(&list.focus_handle(cx), window, Self::on_blur),
cx.on_blur(&focus_handle, window, Self::on_blur),
];
let mut this = Self {
focus_handle,
list,
size: Size::Medium,
selected_value: None,
open: false,
bounds: Bounds::default(),
empty: None,
_subscriptions,
};
this.set_selected_index(selected_index, window, cx);
this
}
pub fn empty<E, F>(mut self, f: F) -> Self
where
E: IntoElement,
F: Fn(&Window, &App) -> E + 'static,
{
self.empty = Some(Box::new(move |window, cx| f(window, cx).into_any_element()));
self
}
pub fn set_selected_index(
&mut self,
selected_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.list.update(cx, |list, cx| {
list.set_selected_index(selected_index, window, cx);
});
self.update_selected_value(window, cx);
}
pub fn set_selected_value(
&mut self,
selected_value: &<D::Item as DropdownItem>::Value,
window: &mut Window,
cx: &mut Context<Self>,
) where
<<D as DropdownDelegate>::Item as DropdownItem>::Value: PartialEq,
{
let delegate = self.list.read(cx).delegate();
let selected_index = delegate.delegate.position(selected_value);
self.set_selected_index(selected_index, window, cx);
}
pub fn selected_index(&self, cx: &App) -> Option<usize> {
self.list.read(cx).selected_index()
}
fn update_selected_value(&mut self, _: &Window, cx: &App) {
self.selected_value = self
.selected_index(cx)
.and_then(|ix| self.list.read(cx).delegate().delegate.get(ix))
.map(|item| item.value().clone());
}
pub fn selected_value(&self) -> Option<&<D::Item as DropdownItem>::Value> {
self.selected_value.as_ref()
}
pub fn focus(&self, window: &mut Window, cx: &mut App) {
self.focus_handle.focus(window, cx);
}
fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// When the dropdown and dropdown menu are both not focused, close the dropdown menu.
if self.list.focus_handle(cx).is_focused(window) || self.focus_handle.is_focused(window) {
return;
}
self.open = false;
cx.notify();
}
fn up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
if !self.open {
return;
}
self.list.focus_handle(cx).focus(window, cx);
cx.propagate();
}
fn down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
if !self.open {
self.open = true;
}
self.list.focus_handle(cx).focus(window, cx);
cx.propagate();
}
fn enter(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
// Propagate the event to the parent view, for example to the Modal to support ENTER to confirm.
cx.propagate();
if !self.open {
self.open = true;
cx.notify();
} else {
self.list.focus_handle(cx).focus(window, cx);
}
}
fn toggle_menu(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
cx.stop_propagation();
self.open = !self.open;
if self.open {
self.list.focus_handle(cx).focus(window, cx);
}
cx.notify();
}
fn escape(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
if !self.open {
cx.propagate();
}
self.open = false;
cx.notify();
}
fn clean(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.set_selected_index(None, window, cx);
cx.emit(DropdownEvent::Confirm(None));
}
/// Set the items for the dropdown.
pub fn set_items(&mut self, items: D, _: &mut Window, cx: &mut Context<Self>)
where
D: DropdownDelegate + 'static,
{
self.list.update(cx, |list, _| {
list.delegate_mut().delegate = items;
});
}
}
impl<D> Render for DropdownState<D>
where
D: DropdownDelegate + 'static,
{
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
impl<D> Dropdown<D>
where
D: DropdownDelegate + 'static,
{
pub fn new(state: &Entity<DropdownState<D>>) -> Self {
Self {
id: ("dropdown", state.entity_id()).into(),
state: state.clone(),
placeholder: None,
size: Size::Medium,
icon: None,
cleanable: false,
title_prefix: None,
empty: None,
width: Length::Auto,
menu_width: Length::Auto,
disabled: false,
}
}
/// Set the width of the dropdown input, default: Length::Auto
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Set the width of the dropdown menu, default: Length::Auto
pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
self.menu_width = width.into();
self
}
/// Set the placeholder for display when dropdown value is empty.
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
/// Set the right icon for the dropdown input, instead of the default arrow icon.
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
/// Set title prefix for the dropdown.
///
/// e.g.: Country: United States
///
/// You should set the label is `Country: `
pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
self.title_prefix = Some(prefix.into());
self
}
/// Set true to show the clear button when the input field is not empty.
pub fn cleanable(mut self) -> Self {
self.cleanable = true;
self
}
/// Set the disable state for the dropdown.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn empty(mut self, el: impl IntoElement) -> Self {
self.empty = Some(el.into_any_element());
self
}
/// Returns the title element for the dropdown input.
fn display_title(&self, _: &Window, cx: &App) -> impl IntoElement {
let default_title = div()
.text_color(cx.theme().text_accent)
.child(
self.placeholder
.clone()
.unwrap_or_else(|| "Please select".into()),
)
.when(self.disabled, |this| this.text_color(cx.theme().text_muted));
let Some(selected_index) = &self.state.read(cx).selected_index(cx) else {
return default_title;
};
let Some(title) = self
.state
.read(cx)
.list
.read(cx)
.delegate()
.delegate
.get(*selected_index)
.map(|item| {
if let Some(el) = item.display_title() {
el
} else if let Some(prefix) = self.title_prefix.as_ref() {
format!("{}{}", prefix, item.title()).into_any_element()
} else {
item.title().into_any_element()
}
})
else {
return default_title;
};
div()
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
.child(title)
}
}
impl<D> Sizable for Dropdown<D>
where
D: DropdownDelegate + 'static,
{
fn with_size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into();
self
}
}
impl<D> EventEmitter<DropdownEvent<D>> for DropdownState<D> where D: DropdownDelegate + 'static {}
impl<D> EventEmitter<DismissEvent> for DropdownState<D> where D: DropdownDelegate + 'static {}
impl<D> Focusable for DropdownState<D>
where
D: DropdownDelegate,
{
fn focus_handle(&self, cx: &App) -> FocusHandle {
if self.open {
self.list.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
impl<D> Focusable for Dropdown<D>
where
D: DropdownDelegate,
{
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.state.focus_handle(cx)
}
}
impl<D> RenderOnce for Dropdown<D>
where
D: DropdownDelegate + 'static,
{
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_focused = self.focus_handle(cx).is_focused(window);
// If the size has change, set size to self.list, to change the QueryInput size.
let old_size = self.state.read(cx).list.read(cx).size;
if old_size != self.size {
self.state
.read(cx)
.list
.clone()
.update(cx, |this, cx| this.set_size(self.size, window, cx));
self.state.update(cx, |this, _| {
this.size = self.size;
});
}
let state = self.state.read(cx);
let show_clean = self.cleanable && state.selected_index(cx).is_some();
let bounds = state.bounds;
let allow_open = !(state.open || self.disabled);
let outline_visible = state.open || is_focused && !self.disabled;
let popup_radius = cx.theme().radius.min(px(8.));
div()
.id(self.id.clone())
.key_context(CONTEXT)
.track_focus(&self.focus_handle(cx))
.on_action(window.listener_for(&self.state, DropdownState::up))
.on_action(window.listener_for(&self.state, DropdownState::down))
.on_action(window.listener_for(&self.state, DropdownState::enter))
.on_action(window.listener_for(&self.state, DropdownState::escape))
.size_full()
.relative()
.input_font_size(self.size)
.child(
div()
.id(ElementId::Name(format!("{}-input", self.id).into()))
.relative()
.flex()
.items_center()
.justify_between()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_sm())
.overflow_hidden()
.input_font_size(self.size)
.map(|this| match self.width {
Length::Definite(l) => this.flex_none().w(l),
Length::Auto => this.w_full(),
})
.when(outline_visible, |this| this.border_color(cx.theme().ring))
.input_size(self.size)
.when(allow_open, |this| {
this.on_click(window.listener_for(&self.state, DropdownState::toggle_menu))
})
.child(
h_flex()
.w_full()
.items_center()
.justify_between()
.gap_1()
.child(
div()
.w_full()
.overflow_hidden()
.whitespace_nowrap()
.truncate()
.child(self.display_title(window, cx)),
)
.when(show_clean, |this| {
this.child(clear_button(cx).map(|this| {
if self.disabled {
this.disabled(true)
} else {
this.on_click(
window.listener_for(&self.state, DropdownState::clean),
)
}
}))
})
.when(!show_clean, |this| {
let icon = match self.icon.clone() {
Some(icon) => icon,
None => {
if state.open {
Icon::new(IconName::CaretUp)
} else {
Icon::new(IconName::CaretDown)
}
}
};
this.child(icon.xsmall().text_color(match self.disabled {
true => cx.theme().text_placeholder,
false => cx.theme().text_muted,
}))
}),
)
.child(
canvas(
{
let state = self.state.clone();
move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds)
},
|_, _, _, _| {},
)
.absolute()
.size_full(),
),
)
.when(state.open, |this| {
this.child(
deferred(
anchored().snap_to_window_with_margin(px(8.)).child(
div()
.occlude()
.map(|this| match self.menu_width {
Length::Auto => this.w(bounds.size.width),
Length::Definite(w) => this.w(w),
})
.child(
v_flex()
.occlude()
.mt_1p5()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.rounded(popup_radius)
.when(cx.theme().shadow, |this| this.shadow_md())
.child(state.list.clone()),
)
.on_mouse_down_out(window.listener_for(
&self.state,
|this, _, window, cx| {
this.escape(&Cancel, window, cx);
},
)),
),
)
.with_priority(1),
)
})
}
}

View File

@@ -1,24 +1,294 @@
use gpui::{Axis, Context, Window};
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 fn h_resizable(
window: &mut Window,
cx: &mut Context<ResizablePanelGroup>,
) -> ResizablePanelGroup {
ResizablePanelGroup::new(window, cx).axis(Axis::Horizontal)
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)
}
pub fn v_resizable(
window: &mut Window,
cx: &mut Context<ResizablePanelGroup>,
) -> ResizablePanelGroup {
ResizablePanelGroup::new(window, cx).axis(Axis::Vertical)
/// 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>,
}

View File

@@ -1,50 +1,60 @@
use std::ops::{Deref, Range};
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,
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 super::resize_handle;
use crate::{h_flex, v_flex, AxisExt};
pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.);
use super::{resizable_panel, resize_handle, ResizableState};
use crate::resizable::PANEL_MIN_SIZE;
use crate::{h_flex, v_flex, AxisExt, ElementExt};
pub enum ResizablePanelEvent {
Resized,
}
#[derive(Clone, Render)]
pub struct DragPanel(pub (EntityId, usize, Axis));
#[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 {
panels: Vec<Entity<ResizablePanel>>,
sizes: Vec<Pixels>,
id: ElementId,
state: Option<Entity<ResizableState>>,
axis: Axis,
size: Option<Pixels>,
bounds: Bounds<Pixels>,
resizing_panel_ix: Option<usize>,
children: Vec<ResizablePanel>,
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
}
impl ResizablePanelGroup {
pub(super) fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
/// Create a new resizable panel group.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
axis: Axis::Horizontal,
sizes: Vec::new(),
panels: Vec::new(),
children: vec![],
state: None,
size: None,
bounds: Bounds::default(),
resizing_panel_ix: None,
on_resize: Rc::new(|_, _, _| {}),
}
}
pub fn load(&mut self, sizes: Vec<Pixels>, panels: Vec<Entity<ResizablePanel>>) {
self.sizes = sizes;
self.panels = panels;
/// 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.
@@ -53,35 +63,23 @@ impl ResizablePanelGroup {
self
}
pub(crate) 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);
/// 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 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)
/// 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
@@ -93,375 +91,222 @@ impl ResizablePanelGroup {
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)
/// 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
}
}
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));
impl<T> From<T> for ResizablePanel
where
T: Into<AnyElement>,
{
fn from(value: T) -> Self {
resizable_panel().child(value.into())
}
}
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(crate) 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(crate) 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 From<ResizablePanelGroup> for ResizablePanel {
fn from(value: ResizablePanelGroup) -> Self {
resizable_panel().child(value)
}
}
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();
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()
};
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())
});
}
// 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);
});
panel.clone()
}))
.child({
canvas(
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _, _| {},
)
.absolute()
.size_full()
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 {
view: cx.entity().clone(),
state: state.clone(),
axis: self.axis,
on_resize: self.on_resize.clone(),
})
}
}
type ContentBuilder = Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>;
type ContentVisible = Rc<Box<dyn Fn(&App) -> bool>>;
/// A resizable panel inside a [`ResizablePanelGroup`].
#[derive(IntoElement)]
pub struct ResizablePanel {
group: Option<WeakEntity<ResizablePanelGroup>>,
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 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>,
/// 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 {
group: None,
panel_ix: 0,
initial_size: None,
size: None,
size_ratio: None,
state: None,
size_range: (PANEL_MIN_SIZE..Pixels::MAX),
axis: Axis::Horizontal,
content_builder: None,
content_view: None,
content_visible: Rc::new(Box::new(|_| true)),
bounds: Bounds::default(),
resize_handle: None,
children: vec![],
visible: true,
}
}
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(crate) fn content_visible<F>(mut self, content_visible: F) -> Self
where
F: Fn(&App) -> bool + 'static,
{
self.content_visible = Rc::new(Box::new(content_visible));
self
}
pub fn content_view(mut self, content: AnyView) -> Self {
self.content_view = Some(content);
/// 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: Pixels) -> Self {
self.initial_size = Some(size);
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.initial_size = Some(size.into());
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();
/// 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 FluentBuilder for ResizablePanel {}
impl ParentElement for ResizablePanel {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
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();
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 total_size = self
.group
.as_ref()
.and_then(|group| group.upgrade())
.map(|group| group.read(cx).total_size());
let view = cx.entity();
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(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))
},
|_, _, _, _| {},
.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(),
)
.absolute()
.size_full()
.flex_basis(initial_size)
})
.when_some(self.content_builder.clone(), |this, c| {
this.child(c(window, cx))
.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())
},
))
})
.when_some(self.content_view.clone(), |this, c| this.child(c))
.when_some(self.resize_handle.take(), |this, c| this.child(c))
}
}
#[allow(clippy::type_complexity)]
struct ResizePanelGroupElement {
state: Entity<ResizableState>,
on_resize: Rc<dyn Fn(&Entity<ResizableState>, &mut Window, &mut App)>,
axis: Axis,
view: Entity<ResizablePanelGroup>,
}
impl IntoElement for ResizePanelGroupElement {
@@ -516,44 +361,43 @@ impl Element for ResizePanelGroupElement {
cx: &mut App,
) {
window.on_mouse_event({
let view = self.view.clone();
let state = self.state.clone();
let axis = self.axis;
let current_ix = view.read(cx).resizing_panel_ix;
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 };
view.update(cx, |view, cx| {
let panel = view
.panels
.get(ix)
.expect("BUG: invalid panel index")
.read(cx);
state.update(cx, |state, cx| {
let panel = state.panels.get(ix).expect("BUG: invalid panel index");
match axis {
Axis::Horizontal => {
view.resize_panels(ix, e.position.x - panel.bounds.left(), window, cx)
state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx)
}
Axis::Vertical => {
view.resize_panels(ix, e.position.y - panel.bounds.top(), window, cx);
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 view = self.view.clone();
let current_ix = view.read(cx).resizing_panel_ix;
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() {
view.update(cx, |view, cx| view.done_resizing(window, cx));
state.update(cx, |state, cx| state.done_resizing(cx));
on_resize(&state, window, cx);
}
}
})

View File

@@ -1,74 +1,227 @@
use std::cell::Cell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, App, Axis, Div, ElementId, InteractiveElement, IntoElement, ParentElement as _,
Pixels, RenderOnce, Stateful, StatefulInteractiveElement, Styled as _, Window,
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 crate::AxisExt as _;
use crate::dock_area::dock::DockPlacement;
use crate::AxisExt;
pub(crate) const HANDLE_PADDING: Pixels = px(8.);
pub(crate) const HANDLE_SIZE: Pixels = px(2.);
#[derive(IntoElement)]
pub(crate) 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,
}
}
}
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(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle {
pub(crate) fn resize_handle<T: 'static, E: 'static + Render>(
id: impl Into<ElementId>,
axis: Axis,
) -> ResizeHandle<T, E> {
ResizeHandle::new(id, axis)
}
impl InteractiveElement for ResizeHandle {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.base.interactivity()
#[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
}
}
impl StatefulInteractiveElement for ResizeHandle {}
#[derive(Default, Debug, Clone)]
struct ResizeHandleState {
active: Cell<bool>,
}
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)),
)
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_selected
} else {
cx.theme().border_variant
};
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)
});
}
}

View File

@@ -373,7 +373,7 @@ impl Render for Root {
})
.size_full()
.font_family(font_family)
.bg(cx.theme().background)
.bg(cx.theme().surface_background)
.text_color(cx.theme().text)
.child(self.view.clone()),
)

View File

@@ -1,38 +1,46 @@
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window,
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Window,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
use crate::Selectable;
use crate::{Selectable, Sizable, Size};
pub mod tab_bar;
#[derive(IntoElement)]
pub struct Tab {
base: Stateful<Div>,
label: AnyElement,
ix: usize,
base: Div,
label: Option<AnyElement>,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
disabled: bool,
selected: bool,
size: Size,
}
impl Tab {
pub fn new(id: impl Into<ElementId>, label: impl IntoElement) -> Self {
let id: ElementId = id.into();
pub fn new() -> Self {
Self {
base: div().id(id),
label: label.into_any_element(),
ix: 0,
base: div(),
label: None,
disabled: false,
selected: false,
prefix: 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
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
self.prefix = Some(prefix.into());
@@ -50,6 +58,18 @@ impl Tab {
self.disabled = disabled;
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 {
@@ -77,34 +97,47 @@ 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 {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) {
(true, false) => (
cx.theme().text,
cx.theme().tab_active_background,
cx.theme().tab_hover_background,
),
(false, false) => (
cx.theme().text_muted,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
),
(true, true) => (
cx.theme().text_muted,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
),
(false, true) => (
cx.theme().text_muted,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
),
};
let (text_color, hover_text_color, bg_color, border_color) =
match (self.selected, self.disabled) {
(true, false) => (
cx.theme().tab_active_foreground,
cx.theme().tab_hover_foreground,
cx.theme().tab_active_background,
cx.theme().border,
),
(false, false) => (
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().border_transparent,
),
(true, true) => (
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().border_disabled,
),
(false, true) => (
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().border_disabled,
),
};
self.base
.h(px(30.))
.px_2()
.id(self.ix)
.h(TITLEBAR_HEIGHT)
.px_4()
.relative()
.flex()
.items_center()
@@ -115,12 +148,19 @@ impl RenderOnce for Tab {
.text_ellipsis()
.text_color(text_color)
.bg(bg_color)
.rounded(cx.theme().radius_lg)
.hover(|this| this.bg(hover_bg_color))
.border_l(px(1.))
.border_r(px(1.))
.border_color(border_color)
.when(!self.selected && !self.disabled, |this| {
this.hover(|this| this.text_color(hover_text_color))
})
.when_some(self.prefix, |this, prefix| {
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))
.on_mouse_down(MouseButton::Left, |_ev, _window, cx| {
cx.stop_propagation();
})
}
}

View File

@@ -1,37 +1,44 @@
use gpui::prelude::FluentBuilder as _;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{
div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
RenderOnce, ScrollHandle, StatefulInteractiveElement as _, Styled, Window,
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::h_flex;
use crate::{h_flex, Sizable, Size, StyledExt};
#[derive(IntoElement)]
pub struct TabBar {
base: Div,
id: ElementId,
scroll_handle: ScrollHandle,
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(id: impl Into<ElementId>) -> Self {
pub fn new() -> Self {
Self {
base: div().px(px(-1.)),
id: id.into(),
base: h_flex().px(px(-1.)),
style: StyleRefinement::default(),
scroll_handle: None,
children: SmallVec::new(),
scroll_handle: ScrollHandle::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 = scroll_handle;
/// Track the scroll of the TabBar.
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.scroll_handle = Some(scroll_handle.clone());
self
}
@@ -46,6 +53,23 @@ impl TabBar {
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 {
@@ -55,30 +79,49 @@ impl ParentElement for TabBar {
}
impl Styled for TabBar {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
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 {
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()
.refine_style(&self.style)
.bg(cx.theme().surface_background)
.child(
div()
.id("border-bottom")
.absolute()
.left_0()
.bottom_0()
.size_full()
.border_b_1()
.border_color(cx.theme().border),
)
.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()
.track_scroll(&self.scroll_handle)
.children(self.children),
.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))
}