chore: make the ui consistent (#19)

Reviewed-on: #19
Co-authored-by: Ren Amamiya <reya@lume.nu>
Co-committed-by: Ren Amamiya <reya@lume.nu>
This commit was merged in pull request #19.
This commit is contained in:
2026-03-12 02:19:59 +00:00
committed by reya
parent 15c5ce7677
commit ccbcc644db
37 changed files with 1935 additions and 1202 deletions

View File

@@ -1,6 +1,5 @@
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::sync::Arc;
use std::time::Duration;
pub use actions::*;
use anyhow::{Context as AnyhowContext, Error};
@@ -24,7 +23,7 @@ use state::{NostrRegistry, upload};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::DropdownMenu;
@@ -42,11 +41,6 @@ mod text;
const ANNOUNCEMENT: &str =
"This conversation is private. Only members can see each other's messages.";
const NO_INBOX: &str = "has not set up messaging relays. \
They will not receive messages you send.";
const NO_ANNOUNCEMENT: &str = "has not set up an encryption key. \
You cannot send messages encrypted with an encryption key to them yet. \
Coop automatically uses your identity to encrypt messages.";
pub fn init(room: WeakEntity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx))
@@ -131,7 +125,7 @@ impl ChatPanel {
});
// Define subject input state
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("Nostr Meetup"));
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
let subject_bar = cx.new(|_cx| false);
// Define subscriptions
@@ -161,7 +155,6 @@ impl ChatPanel {
// Define all functions that will run after the current cycle
cx.defer_in(window, |this, window, cx| {
this.connect(window, cx);
this.handle_notifications(cx);
this.subscribe_room_events(window, cx);
this.get_messages(window, cx);
@@ -187,49 +180,6 @@ impl ChatPanel {
}
}
/// Get all necessary data for each member
fn connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Ok((members, connect)) = self
.room
.read_with(cx, |this, cx| (this.members(), this.connect(cx)))
else {
return;
};
// Run the connect task in background
self.tasks.push(connect);
// Spawn another task to verify after 3 seconds
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(3)).await;
// Verify the connection
this.update_in(cx, |this, _window, cx| {
let persons = PersonRegistry::global(cx);
for member in members.into_iter() {
let profile = persons.read(cx).get(&member, cx);
if profile.announcement().is_none() {
let content = format!("{} {}", profile.name(), NO_ANNOUNCEMENT);
let message = Message::warning(content);
this.insert_message(message, true, cx);
}
if profile.messaging_relays().is_empty() {
let content = format!("{} {}", profile.name(), NO_INBOX);
let message = Message::warning(content);
this.insert_message(message, true, cx);
}
}
})?;
Ok(())
}));
}
/// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
@@ -1047,11 +997,11 @@ impl ChatPanel {
fn render_message_reports(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
h_flex()
.id(SharedString::from(id.to_hex()))
.gap_0p5()
.text_color(cx.theme().danger_active)
.gap_1()
.text_color(cx.theme().text_danger)
.text_xs()
.italic()
.child(Icon::new(IconName::Info).xsmall())
.child(Icon::new(IconName::Info).small())
.child(SharedString::from(
"Failed to send message. Click to see details.",
))

View File

@@ -162,7 +162,7 @@ impl Render for AccountSelector {
.italic()
.text_xs()
.text_center()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
})

View File

@@ -101,7 +101,7 @@ impl Render for ConnectSigner {
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
})

View File

@@ -293,7 +293,7 @@ impl Render for ImportKey {
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
})

View File

@@ -2,16 +2,16 @@ use std::time::Duration;
use anyhow::Error;
use gpui::{
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
};
use nostr_sdk::prelude::*;
use state::KEYRING;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::{divider, v_flex, IconName, Sizable, StyledExt};
use ui::{IconName, Sizable, StyledExt, divider, v_flex};
const MSG: &str = "Store your account keys in a safe location. \
You can restore your account or move to another client anytime you want.";

View File

@@ -15,7 +15,7 @@ use state::NostrRegistry;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
@@ -322,7 +322,7 @@ impl Render for ContactListPanel {
div()
.italic()
.text_xs()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),

View File

@@ -5,8 +5,7 @@ use gpui::{
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{DockPlacement, Panel, PanelEvent};
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use crate::panels::profile;

View File

@@ -13,7 +13,7 @@ use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
@@ -338,7 +338,7 @@ impl Render for MessagingRelayPanel {
div()
.italic()
.text_xs()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),

View File

@@ -14,7 +14,7 @@ use state::{NostrRegistry, upload};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::notification::Notification;
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};

View File

@@ -1,23 +1,23 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use anyhow::{Context as AnyhowContext, Error, anyhow};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, rems, Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, TextAlign, Window,
Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, Window, div, px, rems,
};
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::DropdownMenu;
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
const MSG: &str = "Relay List (or Gossip Relays) are a set of relays \
where you will publish all your events. Others also publish events \
@@ -408,7 +408,7 @@ impl Render for RelayListPanel {
div()
.italic()
.text_xs()
.text_color(cx.theme().danger_active)
.text_color(cx.theme().text_danger)
.child(error.clone()),
)
}),

View File

@@ -3,16 +3,16 @@ use std::rc::Rc;
use chat::RoomKind;
use gpui::prelude::FluentBuilder;
use gpui::{
div, App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window,
App, ClickEvent, InteractiveElement, IntoElement, ParentElement as _, RenderOnce, SharedString,
StatefulInteractiveElement, Styled, Window, div,
};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::dock_area::ClosePanel;
use ui::dock::ClosePanel;
use ui::modal::ModalButtonProps;
use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex};
use crate::dialogs::screening;

View File

@@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry};
use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification;
@@ -503,7 +503,7 @@ impl Render for Sidebar {
.h(TABBAR_HEIGHT)
.border_b_1()
.border_color(cx.theme().border)
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().tab_background)
.child(
TextInput::new(&self.find_input)
.appearance(false)

View File

@@ -18,9 +18,7 @@ use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use title_bar::TitleBar;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::dock::{ClosePanel, DockArea, DockItem, DockPlacement, PanelView};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::{Notification, NotificationKind};
use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
@@ -277,8 +275,8 @@ impl Workspace {
/// Set the center dock layout
fn set_center_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.downgrade();
let greeeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeeter], None, &dock, window, cx);
let greeter = Arc::new(greeter::init(window, cx));
let tabs = DockItem::tabs(vec![greeter], None, &dock, window, cx);
let center = DockItem::split(Axis::Vertical, vec![tabs], &dock, window, cx);
// Update the layout with center dock

View File

@@ -1,4 +1,4 @@
use gpui::{hsla, Hsla, Rgba};
use gpui::{Hsla, Rgba, hsla};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -30,6 +30,8 @@ pub struct ThemeColors {
pub text_muted: Hsla,
pub text_placeholder: Hsla,
pub text_accent: Hsla,
pub text_danger: Hsla,
pub text_warning: Hsla,
// Icon colors
pub icon: Hsla,
@@ -77,11 +79,11 @@ pub struct ThemeColors {
pub ghost_element_disabled: Hsla,
// Tab colors
pub tab_inactive_background: Hsla,
pub tab_inactive_foreground: Hsla,
pub tab_background: Hsla,
pub tab_foreground: Hsla,
pub tab_hover_background: Hsla,
pub tab_active_background: Hsla,
pub tab_active_foreground: Hsla,
pub tab_hover_foreground: Hsla,
// Scrollbar colors
pub scrollbar_thumb_background: Hsla,
@@ -110,8 +112,8 @@ impl ThemeColors {
elevated_surface_background: neutral().light().step_3(),
panel_background: neutral().light().step_1(),
overlay: neutral().light_alpha().step_3(),
title_bar: neutral().light().step_2(),
title_bar_inactive: neutral().light().step_3(),
title_bar: neutral().light().step_3(),
title_bar_inactive: neutral().light().step_1(),
window_border: hsl(240.0, 5.9, 78.0),
border: neutral().light().step_6(),
@@ -125,7 +127,9 @@ impl ThemeColors {
text: neutral().light().step_12(),
text_muted: neutral().light().step_11(),
text_placeholder: neutral().light().step_10(),
text_accent: brand().light().step_11(),
text_accent: brand().light().step_9(),
text_danger: danger().light().step_9(),
text_warning: warning().light().step_9(),
icon: neutral().light().step_11(),
icon_muted: neutral().light().step_10(),
@@ -166,17 +170,17 @@ impl ThemeColors {
ghost_element_selected: neutral().light().step_5(),
ghost_element_disabled: neutral().light_alpha().step_2(),
tab_inactive_background: neutral().light().step_2(),
tab_inactive_foreground: neutral().light().step_11(),
tab_background: neutral().light().step_3(),
tab_foreground: neutral().light().step_11(),
tab_hover_background: neutral().light_alpha().step_4(),
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(),
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().light().step_5(),
scrollbar_track_border: gpui::transparent_black(),
drop_target_background: brand().light_alpha().step_2(),
cursor: hsl(200., 100., 50.),
@@ -192,9 +196,9 @@ impl ThemeColors {
background: neutral().dark().step_1(),
surface_background: neutral().dark().step_2(),
elevated_surface_background: neutral().dark().step_3(),
panel_background: gpui::black(),
panel_background: neutral().dark().step_1(),
overlay: neutral().dark_alpha().step_3(),
title_bar: gpui::transparent_black(),
title_bar: neutral().dark().step_3(),
title_bar_inactive: neutral().dark().step_1(),
window_border: hsl(240.0, 3.7, 28.0),
@@ -209,7 +213,9 @@ impl ThemeColors {
text: neutral().dark().step_12(),
text_muted: neutral().dark().step_11(),
text_placeholder: neutral().dark().step_10(),
text_accent: brand().dark().step_11(),
text_accent: brand().dark().step_9(),
text_danger: danger().dark().step_9(),
text_warning: warning().dark().step_9(),
icon: neutral().dark().step_11(),
icon_muted: neutral().dark().step_10(),
@@ -250,17 +256,17 @@ impl ThemeColors {
ghost_element_selected: neutral().dark().step_5(),
ghost_element_disabled: neutral().dark_alpha().step_2(),
tab_inactive_background: neutral().dark().step_2(),
tab_inactive_foreground: neutral().dark().step_11(),
tab_active_background: neutral().dark().step_3(),
tab_background: neutral().dark().step_3(),
tab_foreground: neutral().dark().step_11(),
tab_hover_background: neutral().dark_alpha().step_4(),
tab_active_background: neutral().dark().step_1(),
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(),
scrollbar_thumb_border: gpui::transparent_black(),
scrollbar_track_background: gpui::transparent_black(),
scrollbar_track_border: neutral().dark().step_5(),
scrollbar_track_border: gpui::transparent_black(),
drop_target_background: brand().dark_alpha().step_2(),
cursor: hsl(200., 100., 50.),

View File

@@ -207,7 +207,7 @@ impl From<ThemeFamily> for Theme {
Theme {
font_size: px(15.),
font_family: font_family.into(),
radius: px(5.),
radius: px(6.),
radius_lg: px(10.),
shadow: true,
scrollbar_mode: ScrollbarMode::default(),

View File

@@ -3,20 +3,26 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent,
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity,
Window,
App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
WeakEntity, Window, div, px,
};
use super::{DockArea, DockItem};
use crate::dock_area::panel::PanelView;
use crate::dock_area::tab_panel::TabPanel;
use crate::resizable::{resize_handle, PANEL_MIN_SIZE};
use crate::StyledExt;
use crate::dock::panel::PanelView;
use crate::dock::tab_panel::TabPanel;
use crate::resizable::{PANEL_MIN_SIZE, resize_handle};
#[derive(Clone, Render)]
#[derive(Clone)]
struct ResizePanel;
impl Render for ResizePanel {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockPlacement {
Center,
@@ -321,6 +327,8 @@ impl Render for Dock {
return div();
}
let cache_style = StyleRefinement::default().absolute().size_full();
div()
.relative()
.overflow_hidden()
@@ -336,7 +344,7 @@ impl Render for Dock {
.map(|this| match &self.panel {
DockItem::Split { view, .. } => this.child(view.clone()),
DockItem::Tabs { view, .. } => this.child(view.clone()),
DockItem::Panel { view, .. } => this.child(view.clone().view()),
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
})
.child(self.render_resize_handle(window, cx))
.child(DockElement {

View File

@@ -2,22 +2,24 @@ use std::sync::Arc;
use gpui::prelude::FluentBuilder;
use gpui::{
actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations,
Edges, Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window,
AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Decorations, Edges, Entity,
EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _,
Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div, px,
};
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
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;
#[allow(clippy::module_inception)]
mod dock;
mod panel;
mod stack_panel;
mod tab_panel;
pub use dock::*;
pub use panel::*;
pub use stack_panel::*;
pub use tab_panel::*;
actions!(dock, [ToggleZoom, ClosePanel]);

View File

@@ -1,5 +1,5 @@
use gpui::{
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render,
AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Render,
SharedString, Window,
};
@@ -21,12 +21,6 @@ pub enum PanelStyle {
TabBar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TitleStyle {
pub background: Hsla,
pub foreground: Hsla,
}
pub trait Panel: EventEmitter<PanelEvent> + Render + Focusable {
/// The name of the panel used to serialize, deserialize and identify the panel.
///

View File

@@ -10,8 +10,8 @@ use smallvec::SmallVec;
use theme::{ActiveTheme, AxisExt as _, CLIENT_SIDE_DECORATION_ROUNDING, Placement};
use super::{DockArea, PanelEvent};
use crate::dock_area::panel::{Panel, PanelView};
use crate::dock_area::tab_panel::TabPanel;
use crate::dock::panel::{Panel, PanelView};
use crate::dock::tab_panel::TabPanel;
use crate::h_flex;
use crate::resizable::{
PANEL_MIN_SIZE, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState,

View File

@@ -10,10 +10,10 @@ use gpui::{
use theme::{ActiveTheme, AxisExt, CLIENT_SIDE_DECORATION_ROUNDING, Placement, TABBAR_HEIGHT};
use crate::button::{Button, ButtonVariants as _};
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::dock::dock::DockPlacement;
use crate::dock::panel::{Panel, PanelView};
use crate::dock::stack_panel::StackPanel;
use crate::dock::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::menu::{DropdownMenu, PopupMenu};
use crate::tab::Tab;
use crate::tab::tab_bar::TabBar;
@@ -42,22 +42,20 @@ impl DragPanel {
impl Render for DragPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
h_flex()
.id("drag-panel")
.cursor_grab()
.py_1()
.px_2()
.w_24()
.flex()
.items_center()
.p_2()
.min_w_24()
.justify_center()
.overflow_hidden()
.whitespace_nowrap()
.rounded(cx.theme().radius_lg)
.rounded(cx.theme().radius)
.text_xs()
.when(cx.theme().shadow, |this| this.shadow_lg())
.text_color(cx.theme().text)
.text_ellipsis()
.when(cx.theme().shadow, |this| this.shadow_xs())
.bg(cx.theme().background)
.text_color(cx.theme().text_accent)
.child(self.panel.title(cx))
}
}
@@ -425,14 +423,13 @@ impl TabPanel {
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);
let has_toolbar = !toolbar.is_empty();
h_flex()
.p_0p5()
.gap_1()
.gap_1p5()
.occlude()
.rounded_full()
.children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded()))
.children(toolbar.into_iter().map(|btn| btn.small().ghost()))
.when(self.zoomed, |this| {
this.child(
Button::new("zoom")
@@ -445,15 +442,11 @@ impl TabPanel {
})),
)
})
.when(has_toolbar, |this| {
this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border))
})
.child(
Button::new("menu")
.icon(IconName::Ellipsis)
.small()
.ghost()
.rounded()
.dropdown_menu({
let zoomable = state.zoomable;
let closable = state.closable;
@@ -578,6 +571,7 @@ impl TabPanel {
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();
let is_bottom_dock = bottom_dock_button.is_some();
if tabs_count == 1 && dock_area.read(cx).panel_style == PanelStyle::Default {
let panel = self.panels.first().unwrap();
@@ -646,7 +640,7 @@ impl TabPanel {
.into_any_element();
}
TabBar::new()
TabBar::new("tab-bar")
.track_scroll(&self.tab_bar_scroll_handle)
.h(TABBAR_HEIGHT)
.when(has_extend_dock_button, |this| {
@@ -659,8 +653,9 @@ impl TabPanel {
.border_b_1()
.h_full()
.border_color(cx.theme().border)
.bg(cx.theme().surface_background)
.px_2()
.bg(cx.theme().tab_background)
.pl_0p5()
.pr_1()
.children(left_dock_button)
.children(bottom_dock_button),
)
@@ -682,16 +677,43 @@ impl TabPanel {
Some(
Tab::new()
.ix(ix)
.label(panel.title(cx))
.py_2()
.tab_bar_prefix(has_extend_dock_button)
.child(panel.title(cx))
.selected(active)
.disabled(disabled)
.suffix(
Button::new("close-{ix}")
.icon(IconName::Close)
.tooltip("Close panel")
.ghost()
.xsmall()
.on_click(cx.listener({
let panel = panel.clone();
move |view, _ev, window, cx| {
view.remove_panel(&panel, window, cx);
}
})),
)
.on_click(cx.listener({
let is_collapsed = self.collapsed;
let dock_area = self.dock_area.clone();
move |view, _, window, cx| {
view.set_active_ix(ix, window, cx);
// Open dock if clicked on the collapsed bottom dock
if is_bottom_dock && is_collapsed {
_ = dock_area.update(cx, |dock_area, cx| {
dock_area.toggle_dock(DockPlacement::Bottom, window, cx);
});
}
}
}))
.when(!disabled, |this| {
this.on_mouse_down(
MouseButton::Middle,
cx.listener({
let panel = panel.clone();
move |view, _, window, cx| {
move |view, _ev, window, cx| {
view.remove_panel(&panel, window, cx);
}
}),
@@ -757,14 +779,15 @@ impl TabPanel {
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()
.px_0p5()
.gap_1()
.border_color(cx.theme().border)
.bg(cx.theme().tab_background)
.child(self.render_toolbar(state, window, cx))
.when_some(right_dock_button, |this, btn| this.child(btn)),
)
@@ -1099,6 +1122,7 @@ impl Focusable for TabPanel {
}
impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel {

View File

@@ -17,7 +17,7 @@ pub mod avatar;
pub mod button;
pub mod checkbox;
pub mod divider;
pub mod dock_area;
pub mod dock;
pub mod group_box;
pub mod history;
pub mod indicator;

View File

@@ -29,10 +29,8 @@ impl NotificationKind {
fn icon(&self, cx: &App) -> Icon {
match self {
Self::Info => Icon::new(IconName::Info).text_color(cx.theme().icon),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().warning_foreground),
Self::Success => {
Icon::new(IconName::CheckCircle).text_color(cx.theme().secondary_foreground)
}
Self::Success => Icon::new(IconName::CheckCircle).text_color(cx.theme().icon_accent),
Self::Warning => Icon::new(IconName::Warning).text_color(cx.theme().text_warning),
Self::Error => {
Icon::new(IconName::CloseCircle).text_color(cx.theme().danger_foreground)
}
@@ -352,8 +350,8 @@ impl Render for Notification {
.child(
div()
.absolute()
.top_2()
.right_2()
.top(px(6.5))
.right(px(6.5))
.invisible()
.group_hover("", |this| this.visible())
.child(

View File

@@ -9,7 +9,7 @@ use gpui::{
};
use theme::{ActiveTheme, AxisExt};
use crate::dock_area::dock::DockPlacement;
use crate::dock::DockPlacement;
pub(crate) const HANDLE_PADDING: Pixels = px(4.);
pub(crate) const HANDLE_SIZE: Pixels = px(1.);

View File

@@ -1,4 +1,4 @@
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px};
use serde::{Deserialize, Serialize};
use theme::ActiveTheme;
@@ -46,6 +46,30 @@ pub trait StyledExt: Styled + Sized {
self.flex().flex_col()
}
/// Apply paddings to the element.
fn paddings<L>(self, paddings: impl Into<Edges<L>>) -> Self
where
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
{
let paddings = paddings.into();
self.pt(paddings.top.into())
.pb(paddings.bottom.into())
.pl(paddings.left.into())
.pr(paddings.right.into())
}
/// Apply margins to the element.
fn margins<L>(self, margins: impl Into<Edges<L>>) -> Self
where
L: Into<DefiniteLength> + Clone + Default + std::fmt::Debug + PartialEq,
{
let margins = margins.into();
self.mt(margins.top.into())
.mb(margins.bottom.into())
.ml(margins.left.into())
.mr(margins.right.into())
}
font_weight!(font_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT);
font_weight!(font_light, LIGHT);

View File

@@ -1,74 +1,557 @@
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Window,
};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use std::rc::Rc;
use crate::{Selectable, Sizable, Size};
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, ClickEvent, Div, Edges, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
div, px, relative,
};
use theme::ActiveTheme;
use crate::{Icon, IconName, Selectable, Sizable, Size, StyledExt, h_flex};
pub mod tab_bar;
/// Tab variants.
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
pub enum TabVariant {
#[default]
Tab,
Outline,
Pill,
Segmented,
Underline,
}
impl TabVariant {
fn height(&self, size: Size) -> Pixels {
match size {
Size::XSmall => match self {
TabVariant::Underline => px(26.),
_ => px(20.),
},
Size::Small => match self {
TabVariant::Underline => px(30.),
_ => px(24.),
},
Size::Large => match self {
TabVariant::Underline => px(44.),
_ => px(36.),
},
_ => match self {
TabVariant::Underline => px(36.),
_ => px(32.),
},
}
}
fn inner_height(&self, size: Size) -> Pixels {
match size {
Size::XSmall => match self {
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(18.),
TabVariant::Segmented => px(16.),
TabVariant::Underline => px(20.),
},
Size::Small => match self {
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(22.),
TabVariant::Segmented => px(18.),
TabVariant::Underline => px(22.),
},
Size::Large => match self {
TabVariant::Tab | TabVariant::Outline | TabVariant::Pill => px(36.),
TabVariant::Segmented => px(28.),
TabVariant::Underline => px(32.),
},
_ => match self {
TabVariant::Tab => px(30.),
TabVariant::Outline | TabVariant::Pill => px(26.),
TabVariant::Segmented => px(24.),
TabVariant::Underline => px(26.),
},
}
}
/// Default px(12) to match panel px_3, See [`crate::dock::TabPanel`]
fn inner_paddings(&self, size: Size) -> Edges<Pixels> {
let mut padding_x = match size {
Size::XSmall => px(8.),
Size::Small => px(10.),
Size::Large => px(16.),
_ => px(12.),
};
if matches!(self, TabVariant::Underline) {
padding_x = px(0.);
}
Edges {
left: padding_x,
right: padding_x,
..Default::default()
}
}
fn inner_margins(&self, size: Size) -> Edges<Pixels> {
match size {
Size::XSmall => match self {
TabVariant::Underline => Edges {
top: px(1.),
bottom: px(2.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
Size::Small => match self {
TabVariant::Underline => Edges {
top: px(2.),
bottom: px(3.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
Size::Large => match self {
TabVariant::Underline => Edges {
top: px(5.),
bottom: px(6.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
_ => match self {
TabVariant::Underline => Edges {
top: px(3.),
bottom: px(4.),
..Default::default()
},
_ => Edges::all(px(0.)),
},
}
}
fn normal(&self, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
borders: Edges::all(px(1.)),
border_color: cx.theme().border,
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().text,
bg: gpui::transparent_black(),
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
inner_bg: gpui::transparent_black(),
borders: Edges {
bottom: px(2.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
}
}
fn hovered(&self, selected: bool, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().secondary_foreground,
bg: cx.theme().secondary_hover,
borders: Edges::all(px(1.)),
border_color: cx.theme().border,
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().secondary_foreground,
bg: cx.theme().secondary_background,
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
inner_bg: if selected {
cx.theme().background
} else {
gpui::transparent_black()
},
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().tab_foreground,
bg: gpui::transparent_black(),
inner_bg: gpui::transparent_black(),
borders: Edges {
bottom: px(2.),
..Default::default()
},
border_color: gpui::transparent_black(),
..Default::default()
},
}
}
fn selected(&self, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().tab_active_foreground,
bg: cx.theme().tab_active_background,
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
border_color: cx.theme().border,
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().text_accent,
bg: gpui::transparent_black(),
borders: Edges::all(px(1.)),
border_color: cx.theme().element_active,
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: cx.theme().element_foreground,
bg: cx.theme().element_background,
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().tab_active_foreground,
bg: gpui::transparent_black(),
inner_bg: cx.theme().background,
shadow: true,
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().tab_active_foreground,
bg: gpui::transparent_black(),
borders: Edges {
bottom: px(2.),
..Default::default()
},
border_color: cx.theme().element_active,
..Default::default()
},
}
}
fn disabled(&self, selected: bool, cx: &App) -> TabStyle {
match self {
TabVariant::Tab => TabStyle {
fg: cx.theme().text_muted,
bg: gpui::transparent_black(),
border_color: if selected {
cx.theme().border
} else {
gpui::transparent_black()
},
borders: Edges {
left: px(1.),
right: px(1.),
..Default::default()
},
..Default::default()
},
TabVariant::Outline => TabStyle {
fg: cx.theme().text_muted,
bg: gpui::transparent_black(),
borders: Edges::all(px(1.)),
border_color: if selected {
cx.theme().element_active
} else {
cx.theme().border
},
..Default::default()
},
TabVariant::Pill => TabStyle {
fg: if selected {
cx.theme().element_foreground.opacity(0.5)
} else {
cx.theme().text_muted
},
bg: if selected {
cx.theme().element_background.opacity(0.5)
} else {
gpui::transparent_black()
},
..Default::default()
},
TabVariant::Segmented => TabStyle {
fg: cx.theme().text_muted,
bg: cx.theme().tab_background,
inner_bg: if selected {
cx.theme().background
} else {
gpui::transparent_black()
},
..Default::default()
},
TabVariant::Underline => TabStyle {
fg: cx.theme().text_muted,
bg: gpui::transparent_black(),
border_color: if selected {
cx.theme().border
} else {
gpui::transparent_black()
},
borders: Edges {
bottom: px(2.),
..Default::default()
},
..Default::default()
},
}
}
pub(super) fn tab_bar_radius(&self, size: Size, cx: &App) -> Pixels {
if *self != TabVariant::Segmented {
return px(0.);
}
match size {
Size::XSmall | Size::Small => cx.theme().radius,
Size::Large => cx.theme().radius_lg,
_ => cx.theme().radius_lg,
}
}
fn radius(&self, size: Size, cx: &App) -> Pixels {
match self {
TabVariant::Outline | TabVariant::Pill => px(99.),
TabVariant::Segmented => match size {
Size::XSmall | Size::Small => cx.theme().radius,
Size::Large => cx.theme().radius_lg,
_ => cx.theme().radius_lg,
},
_ => px(0.),
}
}
fn inner_radius(&self, size: Size, cx: &App) -> Pixels {
match self {
TabVariant::Segmented => match size {
Size::Large => self.tab_bar_radius(size, cx) - px(3.),
_ => self.tab_bar_radius(size, cx) - px(2.),
},
_ => px(0.),
}
}
}
#[allow(dead_code)]
struct TabStyle {
borders: Edges<Pixels>,
border_color: Hsla,
bg: Hsla,
fg: Hsla,
shadow: bool,
inner_bg: Hsla,
}
impl Default for TabStyle {
fn default() -> Self {
TabStyle {
borders: Edges::all(px(0.)),
border_color: gpui::transparent_white(),
bg: gpui::transparent_white(),
fg: gpui::transparent_white(),
shadow: false,
inner_bg: gpui::transparent_white(),
}
}
}
#[allow(clippy::type_complexity)]
/// A Tab element for the [`super::TabBar`].
#[derive(IntoElement)]
pub struct Tab {
ix: usize,
base: Div,
label: Option<AnyElement>,
pub(super) label: Option<SharedString>,
icon: Option<Icon>,
prefix: Option<AnyElement>,
pub(super) tab_bar_prefix: Option<bool>,
suffix: Option<AnyElement>,
disabled: bool,
selected: bool,
children: Vec<AnyElement>,
variant: TabVariant,
size: Size,
pub(super) disabled: bool,
pub(super) selected: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl Tab {
pub fn new() -> Self {
Self {
ix: 0,
base: div(),
label: None,
disabled: false,
selected: false,
prefix: None,
suffix: None,
size: Size::default(),
}
impl From<&'static str> for Tab {
fn from(label: &'static str) -> Self {
Self::new().label(label)
}
}
/// Set label for the tab.
pub fn label(mut self, label: impl Into<AnyElement>) -> Self {
self.label = Some(label.into());
self
impl From<String> for Tab {
fn from(label: String) -> Self {
Self::new().label(label)
}
}
/// Set the left side of the tab
pub fn prefix(mut self, prefix: impl Into<AnyElement>) -> Self {
self.prefix = Some(prefix.into());
self
impl From<SharedString> for Tab {
fn from(label: SharedString) -> Self {
Self::new().label(label)
}
}
/// Set the right side of the tab
pub fn suffix(mut self, suffix: impl Into<AnyElement>) -> Self {
self.suffix = Some(suffix.into());
self
impl From<Icon> for Tab {
fn from(icon: Icon) -> Self {
Self::default().icon(icon)
}
}
/// Set disabled state to the tab
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Set index to the tab.
pub fn ix(mut self, ix: usize) -> Self {
self.ix = ix;
self
impl From<IconName> for Tab {
fn from(icon_name: IconName) -> Self {
Self::default().icon(Icon::new(icon_name))
}
}
impl Default for Tab {
fn default() -> Self {
Self::new()
Self {
ix: 0,
base: div(),
label: None,
icon: None,
tab_bar_prefix: None,
children: Vec::new(),
disabled: false,
selected: false,
prefix: None,
suffix: None,
variant: TabVariant::default(),
size: Size::default(),
on_click: None,
}
}
}
impl Tab {
/// Create a new tab with a label.
pub fn new() -> Self {
Self::default()
}
/// Set label for the tab.
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
/// Set icon for the tab.
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
/// Set Tab Variant.
pub fn with_variant(mut self, variant: TabVariant) -> Self {
self.variant = variant;
self
}
/// Use Pill variant.
pub fn pill(mut self) -> Self {
self.variant = TabVariant::Pill;
self
}
/// Use outline variant.
pub fn outline(mut self) -> Self {
self.variant = TabVariant::Outline;
self
}
/// Use Segmented variant.
pub fn segmented(mut self) -> Self {
self.variant = TabVariant::Segmented;
self
}
/// Use Underline variant.
pub fn underline(mut self) -> Self {
self.variant = TabVariant::Underline;
self
}
/// Set the left side of the tab
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
self.prefix = Some(prefix.into_any_element());
self
}
/// Set the right side of the tab
pub fn suffix(mut self, suffix: impl IntoElement) -> Self {
self.suffix = Some(suffix.into_any_element());
self
}
/// Set disabled state to the tab, default false.
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Set the click handler for the tab.
pub fn on_click(
mut self,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Rc::new(on_click));
self
}
/// Set index to the tab.
pub(crate) fn ix(mut self, ix: usize) -> Self {
self.ix = ix;
self
}
/// Set if the tab bar has a prefix.
pub(crate) fn tab_bar_prefix(mut self, tab_bar_prefix: bool) -> Self {
self.tab_bar_prefix = Some(tab_bar_prefix);
self
}
}
impl ParentElement for Tab {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
@@ -105,62 +588,115 @@ impl Sizable for Tab {
}
impl RenderOnce for Tab {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
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,
),
};
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let mut tab_style = if self.selected {
self.variant.selected(cx)
} else {
self.variant.normal(cx)
};
let mut hover_style = self.variant.hovered(self.selected, cx);
if self.disabled {
tab_style = self.variant.disabled(self.selected, cx);
hover_style = self.variant.disabled(self.selected, cx);
}
let tab_bar_prefix = self.tab_bar_prefix.unwrap_or_default();
if !tab_bar_prefix && self.ix == 0 && self.variant == TabVariant::Tab {
tab_style.borders.left = px(0.);
hover_style.borders.left = px(0.);
}
let radius = self.variant.radius(self.size, cx);
let inner_radius = self.variant.inner_radius(self.size, cx);
let inner_paddings = self.variant.inner_paddings(self.size);
let inner_margins = self.variant.inner_margins(self.size);
let inner_height = self.variant.inner_height(self.size);
let height = self.variant.height(self.size);
self.base
.id(self.ix)
.h(TABBAR_HEIGHT)
.px_4()
.relative()
.flex()
.flex_wrap()
.gap_1()
.items_center()
.flex_shrink_0()
.cursor_pointer()
.h(height)
.overflow_hidden()
.text_xs()
.text_ellipsis()
.text_color(text_color)
.bg(bg_color)
.border_l(px(1.))
.border_r(px(1.))
.border_color(border_color)
.text_color(tab_style.fg)
.map(|this| match self.size {
Size::XSmall => this.text_xs(),
Size::Large => this.text_base(),
_ => this.text_sm(),
})
.bg(tab_style.bg)
.border_l(tab_style.borders.left)
.border_r(tab_style.borders.right)
.border_t(tab_style.borders.top)
.border_b(tab_style.borders.bottom)
.border_color(tab_style.border_color)
.rounded(radius)
.when(!self.selected && !self.disabled, |this| {
this.hover(|this| this.text_color(hover_text_color))
this.hover(|this| {
this.text_color(hover_style.fg)
.bg(hover_style.bg)
.border_l(hover_style.borders.left)
.border_r(hover_style.borders.right)
.border_t(hover_style.borders.top)
.border_b(hover_style.borders.bottom)
.border_color(hover_style.border_color)
.rounded(radius)
})
})
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
h_flex()
.flex_1()
.h(inner_height)
.line_height(relative(1.))
.whitespace_nowrap()
.items_center()
.justify_center()
.overflow_hidden()
.margins(inner_margins)
.flex_shrink_0()
.map(|this| match self.icon {
Some(icon) => {
this.w(inner_height * 1.25)
.child(icon.map(|this| match self.size {
Size::XSmall => this.size_2p5(),
Size::Small => this.size_3p5(),
Size::Large => this.size_4(),
_ => this.size_4(),
}))
}
None => this
.paddings(inner_paddings)
.map(|this| match self.label {
Some(label) => this.child(label),
None => this,
})
.children(self.children),
})
.bg(tab_style.inner_bg)
.rounded(inner_radius)
.when(tab_style.shadow, |this| this.shadow_xs())
.hover(|this| this.bg(hover_style.inner_bg).rounded(inner_radius)),
)
.when_some(self.suffix, |this, suffix| {
this.child(div().pr_2().child(suffix))
})
.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| {
.on_mouse_down(MouseButton::Left, |_, _, cx| {
// Stop propagation behavior, for works on TitleBar.
// https://github.com/longbridge/gpui-component/issues/1836
cx.stop_propagation();
})
.when(!self.disabled, |this| {
this.when_some(self.on_click.clone(), |this, on_click| {
this.on_click(move |event, window, cx| on_click(event, window, cx))
})
})
}
}

View File

@@ -1,41 +1,92 @@
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce,
ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
AnyElement, App, Corner, Div, Edges, ElementId, InteractiveElement, IntoElement, ParentElement,
RenderOnce, ScrollHandle, Stateful, StatefulInteractiveElement as _, StyleRefinement, Styled,
Window, div, px,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{h_flex, Sizable, Size, StyledExt};
use super::{Tab, TabVariant};
use crate::button::{Button, ButtonVariants as _};
use crate::menu::{DropdownMenu as _, PopupMenuItem};
use crate::{IconName, Selectable, Sizable, Size, StyledExt, h_flex};
#[allow(clippy::type_complexity)]
/// A TabBar element that contains multiple [`Tab`] items.
#[derive(IntoElement)]
pub struct TabBar {
base: Div,
base: Stateful<Div>,
style: StyleRefinement,
scroll_handle: Option<ScrollHandle>,
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
children: SmallVec<[Tab; 2]>,
last_empty_space: AnyElement,
children: SmallVec<[AnyElement; 2]>,
selected_index: Option<usize>,
variant: TabVariant,
size: Size,
menu: bool,
on_click: Option<Rc<dyn Fn(&usize, &mut Window, &mut App) + 'static>>,
}
impl TabBar {
pub fn new() -> Self {
/// Create a new TabBar.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: h_flex().px(px(-1.)),
base: div().id(id).px(px(-1.)),
style: StyleRefinement::default(),
scroll_handle: None,
children: SmallVec::new(),
scroll_handle: None,
prefix: None,
suffix: None,
variant: TabVariant::default(),
size: Size::default(),
last_empty_space: div().w_3().into_any_element(),
selected_index: None,
on_click: None,
menu: false,
}
}
/// Set the Tab variant, all children will inherit the variant.
pub fn with_variant(mut self, variant: TabVariant) -> Self {
self.variant = variant;
self
}
/// Set the Tab variant to Pill, all children will inherit the variant.
pub fn pill(mut self) -> Self {
self.variant = TabVariant::Pill;
self
}
/// Set the Tab variant to Outline, all children will inherit the variant.
pub fn outline(mut self) -> Self {
self.variant = TabVariant::Outline;
self
}
/// Set the Tab variant to Segmented, all children will inherit the variant.
pub fn segmented(mut self) -> Self {
self.variant = TabVariant::Segmented;
self
}
/// Set the Tab variant to Underline, all children will inherit the variant.
pub fn underline(mut self) -> Self {
self.variant = TabVariant::Underline;
self
}
/// Set whether to show the menu button when tabs overflow, default is false.
pub fn menu(mut self, menu: bool) -> Self {
self.menu = menu;
self
}
/// Track the scroll of the TabBar.
pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.scroll_handle = Some(scroll_handle.clone());
@@ -54,27 +105,39 @@ impl TabBar {
self
}
/// Add children of the TabBar, all children will inherit the variant.
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Tab>>) -> Self {
self.children.extend(children.into_iter().map(Into::into));
self
}
/// Add child of the TabBar, tab will inherit the variant.
pub fn child(mut self, child: impl Into<Tab>) -> Self {
self.children.push(child.into());
self
}
/// Set the selected index of the TabBar.
pub fn selected_index(mut self, index: usize) -> Self {
self.selected_index = Some(index);
self
}
/// Set the last empty space element of the TabBar.
pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self {
self.last_empty_space = last_empty_space.into_any_element();
self
}
#[cfg(not(target_os = "windows"))]
pub fn height(window: &mut Window) -> Pixels {
(1.75 * window.rem_size()).max(px(36.))
}
}
impl Default for TabBar {
fn default() -> Self {
Self::new()
}
}
impl ParentElement for TabBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
/// Set the on_click callback of the TabBar, the first parameter is the index of the clicked tab.
///
/// When this is set, the children's on_click will be ignored.
pub fn on_click<F>(mut self, on_click: F) -> Self
where
F: Fn(&usize, &mut Window, &mut App) + 'static,
{
self.on_click = Some(Rc::new(on_click));
self
}
}
@@ -92,37 +155,136 @@ impl Sizable for TabBar {
}
impl RenderOnce for TabBar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let default_gap = match self.size {
Size::Small | Size::XSmall => px(8.),
Size::Large => px(16.),
_ => px(12.),
};
let (bg, paddings, gap) = match self.variant {
TabVariant::Tab => {
let padding = Edges::all(px(0.));
(cx.theme().tab_background, padding, px(0.))
}
TabVariant::Outline => {
let padding = Edges::all(px(0.));
(gpui::transparent_black(), padding, default_gap)
}
TabVariant::Pill => {
let padding = Edges::all(px(0.));
(gpui::transparent_black(), padding, px(4.))
}
TabVariant::Segmented => {
let padding_x = match self.size {
Size::XSmall => px(2.),
Size::Small => px(3.),
_ => px(4.),
};
let padding = Edges {
left: padding_x,
right: padding_x,
..Default::default()
};
(cx.theme().tab_background, padding, px(2.))
}
TabVariant::Underline => {
// This gap is same as the tab inner_paddings
let gap = match self.size {
Size::XSmall => px(10.),
Size::Small => px(12.),
Size::Large => px(20.),
_ => px(16.),
};
(gpui::transparent_black(), Edges::all(px(0.)), gap)
}
};
let mut item_labels = Vec::new();
let selected_index = self.selected_index;
let on_click = self.on_click.clone();
self.base
.group("tab-bar")
.relative()
.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),
.flex()
.items_center()
.bg(bg)
.text_color(cx.theme().tab_foreground)
.when(
self.variant == TabVariant::Underline || self.variant == TabVariant::Tab,
|this| {
this.child(
div()
.id("border-b")
.absolute()
.left_0()
.bottom_0()
.size_full()
.border_b_1()
.border_color(cx.theme().border),
)
},
)
.text_color(cx.theme().text)
.rounded(self.variant.tab_bar_radius(self.size, cx))
.paddings(paddings)
.refine_style(&self.style)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
h_flex()
.id("tabs")
.flex_grow()
.flex_1()
.overflow_x_scroll()
.when_some(self.scroll_handle, |this, scroll_handle| {
this.track_scroll(&scroll_handle)
})
.children(self.children)
.when(self.suffix.is_some(), |this| {
.gap(gap)
.children(self.children.into_iter().enumerate().map(|(ix, child)| {
item_labels.push((child.label.clone(), child.disabled));
let tab_bar_prefix = child.tab_bar_prefix.unwrap_or(true);
child
.ix(ix)
.tab_bar_prefix(tab_bar_prefix)
.with_variant(self.variant)
.with_size(self.size)
.when_some(self.selected_index, |this, selected_ix| {
this.selected(selected_ix == ix)
})
.when_some(self.on_click.clone(), move |this, on_click| {
this.on_click(move |_, window, cx| on_click(&ix, window, cx))
})
}))
.when(self.suffix.is_some() || self.menu, |this| {
this.child(self.last_empty_space)
}),
)
.when(self.menu, |this| {
this.child(
Button::new("more")
.xsmall()
.ghost()
.icon(IconName::ChevronDown)
.dropdown_menu(move |mut this, _, _| {
this = this.scrollable(true);
for (ix, (label, disabled)) in item_labels.iter().enumerate() {
this = this.item(
PopupMenuItem::new(label.clone().unwrap_or_default())
.checked(selected_index == Some(ix))
.disabled(*disabled)
.when_some(on_click.clone(), |this, on_click| {
this.on_click(move |_, window, cx| {
on_click(&ix, window, cx)
})
}),
)
}
this
})
.anchor(Corner::TopRight),
)
})
.when_some(self.suffix, |this, suffix| this.child(suffix))
}
}