diff --git a/crates/story/examples/dock.rs b/crates/story/examples/dock.rs index 8ecbc405a3..b7feb53e7c 100644 --- a/crates/story/examples/dock.rs +++ b/crates/story/examples/dock.rs @@ -5,6 +5,7 @@ use gpui_component::{ button::{Button, ButtonVariants as _}, dock::{ClosePanel, DockArea, DockAreaState, DockEvent, DockItem, DockPlacement, ToggleZoom}, menu::DropdownMenu, + status_bar::StatusBar, }; use gpui_component_assets::Assets; @@ -513,7 +514,40 @@ impl Render for StoryWorkspace { .flex() .flex_col() .child(self.title_bar.clone()) - .child(self.dock_area.clone()) + .child(div().flex_1().min_h_0().child(self.dock_area.clone())) + .child( + StatusBar::new() + .left( + Button::new("toggle-left-dock").ghost().xsmall() + .icon(IconName::PanelLeft) + .tooltip("Toggle Left Dock") + .on_click(cx.listener(|this, _, window, cx| { + this.dock_area.update(cx, |area, cx| { + area.toggle_dock(DockPlacement::Left, window, cx); + }); + })), + ) + .left( + Button::new("toggle-bottom-dock").ghost().xsmall() + .icon(IconName::PanelBottom) + .tooltip("Toggle Bottom Dock") + .on_click(cx.listener(|this, _, window, cx| { + this.dock_area.update(cx, |area, cx| { + area.toggle_dock(DockPlacement::Bottom, window, cx); + }); + })), + ) + .child( + Button::new("toggle-right-dock").ghost().xsmall() + .icon(IconName::PanelRight) + .tooltip("Toggle Right Dock") + .on_click(cx.listener(|this, _, window, cx| { + this.dock_area.update(cx, |area, cx| { + area.toggle_dock(DockPlacement::Right, window, cx); + }); + })), + ), + ) .children(sheet_layer) .children(dialog_layer) .children(notification_layer) diff --git a/crates/story/examples/editor.rs b/crates/story/examples/editor.rs index d78159c18b..9ef365a09d 100644 --- a/crates/story/examples/editor.rs +++ b/crates/story/examples/editor.rs @@ -20,6 +20,7 @@ use gpui_component::{ }, list::ListItem, resizable::{h_resizable, resizable_panel}, + status_bar::StatusBar, tree::{TreeItem, TreeState, tree}, v_flex, }; @@ -972,7 +973,7 @@ impl Example { &self, _: &mut Window, cx: &mut Context, - ) -> impl IntoElement { + ) -> Button { Button::new("line-number") .when(self.line_number, |this| this.icon(IconName::Check)) .label("Line Number") @@ -987,7 +988,7 @@ impl Example { })) } - fn render_soft_wrap_button(&self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_soft_wrap_button(&self, _: &mut Window, cx: &mut Context) -> Button { Button::new("soft-wrap") .ghost() .xsmall() @@ -1006,7 +1007,7 @@ impl Example { &self, _: &mut Window, cx: &mut Context, - ) -> impl IntoElement { + ) -> Button { Button::new("show-whitespace") .ghost() .xsmall() @@ -1025,7 +1026,7 @@ impl Example { &self, _: &mut Window, cx: &mut Context, - ) -> impl IntoElement { + ) -> Button { Button::new("indent-guides") .ghost() .xsmall() @@ -1040,7 +1041,7 @@ impl Example { })) } - fn render_folding_button(&self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_folding_button(&self, _: &mut Window, cx: &mut Context) -> Button { Button::new("folding") .ghost() .xsmall() @@ -1076,7 +1077,7 @@ impl Example { &self, _: &mut Window, cx: &mut Context, - ) -> impl IntoElement { + ) -> Button { Button::new("scroll-beyond-last-line") .ghost() .xsmall() @@ -1097,7 +1098,7 @@ impl Example { &self, _: &mut Window, cx: &mut Context, - ) -> impl IntoElement { + ) -> Button { Button::new("cursor-surrounding-lines") .ghost() .xsmall() @@ -1114,7 +1115,7 @@ impl Example { })) } - fn render_go_to_line_button(&self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_go_to_line_button(&self, _: &mut Window, cx: &mut Context) -> Button { let position = self.editor.read(cx).cursor_position(); let cursor = self.editor.read(cx).cursor(); @@ -1127,6 +1128,7 @@ impl Example { position.character + 1, cursor )) + .tooltip("Go to Line/Column") .on_click(cx.listener(Self::go_to_line)) } } @@ -1173,27 +1175,15 @@ impl Render for Example { ), ) .child( - h_flex() - .justify_between() - .text_sm() - .bg(cx.theme().background) - .py_1p5() - .px_4() - .border_t_1() - .border_color(cx.theme().border) - .text_color(cx.theme().muted_foreground) - .child( - h_flex() - .gap_3() - .child(self.render_line_number_button(window, cx)) - .child(self.render_soft_wrap_button(window, cx)) - .child(self.render_show_whitespaces_button(window, cx)) - .child(self.render_indent_guides_button(window, cx)) - .child(self.render_folding_button(window, cx)) - .child(self.render_scroll_beyond_last_line_button(window, cx)) - .child(self.render_cursor_surrounding_lines_button(window, cx)), - ) - .child(self.render_go_to_line_button(window, cx)), + StatusBar::new() + .left(self.render_line_number_button(window, cx)) + .left(self.render_soft_wrap_button(window, cx)) + .left(self.render_show_whitespaces_button(window, cx)) + .left(self.render_indent_guides_button(window, cx)) + .left(self.render_folding_button(window, cx)) + .left(self.render_scroll_beyond_last_line_button(window, cx)) + .left(self.render_cursor_surrounding_lines_button(window, cx)) + .right(self.render_go_to_line_button(window, cx)), ), ) } diff --git a/crates/story/src/gallery.rs b/crates/story/src/gallery.rs index adad3e59e2..9d52832f70 100644 --- a/crates/story/src/gallery.rs +++ b/crates/story/src/gallery.rs @@ -1,9 +1,13 @@ use gpui::{prelude::*, *}; use gpui_component::{ - ActiveTheme as _, Icon, IconName, h_flex, + ActiveTheme as _, Icon, IconName, Sizable as _, + button::{Button, ButtonVariants as _}, + h_flex, input::{Input, InputEvent, InputState}, resizable::{h_resizable, resizable_panel}, + separator::Separator, sidebar::{Sidebar, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem}, + status_bar::StatusBar, v_flex, }; @@ -84,6 +88,7 @@ impl Gallery { StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), + StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), StoryContainer::panel::(window, cx), @@ -162,7 +167,10 @@ impl Render for Gallery { ("".into(), "".into()) }; - h_resizable("gallery-container") + let current_story = story_name.clone(); + let total_components: usize = self.stories.iter().map(|(_, items)| items.len()).sum(); + + let body = h_resizable("gallery-container") .child( resizable_panel() .size(px(255.)) @@ -300,6 +308,31 @@ impl Render for Gallery { }), ) .into_any_element(), + ); + + v_flex() + .size_full() + .child(div().flex_1().min_h_0().child(body)) + .child( + StatusBar::new() + .child(Icon::new(IconName::GalleryVerticalEnd).xsmall()) + .child(format!("{total_components} components")) + .child(Separator::vertical()) + .when(!current_story.is_empty(), |this| { + this.child(current_story.clone()) + }) + .right(cx.theme().theme_name().clone()) + .right(format!("v{}", env!("CARGO_PKG_VERSION"))) + .right( + Button::new("assistant") + .ghost() + .xsmall() + .icon(IconName::Github) + .tooltip("GPUI Component GitHub repository") + .on_click(|_, _, cx| { + cx.open_url("https://github.com/longbridge/gpui-component") + }), + ), ) } } diff --git a/crates/story/src/stories/mod.rs b/crates/story/src/stories/mod.rs index 7a93c73e39..2674dbbce4 100644 --- a/crates/story/src/stories/mod.rs +++ b/crates/story/src/stories/mod.rs @@ -49,6 +49,7 @@ mod sidebar_story; mod skeleton_story; mod slider_story; mod spinner_story; +mod status_bar_story; mod stepper_story; mod switch_story; mod table_story; @@ -110,6 +111,7 @@ pub use sidebar_story::SidebarStory; pub use skeleton_story::SkeletonStory; pub use slider_story::SliderStory; pub use spinner_story::SpinnerStory; +pub use status_bar_story::StatusBarStory; pub use stepper_story::StepperStory; pub use switch_story::SwitchStory; pub use table_story::TableStory; diff --git a/crates/story/src/stories/status_bar_story.rs b/crates/story/src/stories/status_bar_story.rs new file mode 100644 index 0000000000..26cb602236 --- /dev/null +++ b/crates/story/src/stories/status_bar_story.rs @@ -0,0 +1,184 @@ +use gpui::{ + App, AppContext, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement, Render, + Styled, Window, +}; +use gpui_component::{ + ActiveTheme as _, Icon, IconName, Sizable as _, WindowExt as _, + button::{Button, ButtonVariants as _}, + dock::PanelControl, + h_flex, + progress::ProgressCircle, + separator::Separator, + status_bar::StatusBar, + v_flex, +}; + +use crate::section; + +pub struct StatusBarStory { + focus_handle: gpui::FocusHandle, +} + +impl StatusBarStory { + fn new(_: &mut Window, cx: &mut Context) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } + + pub fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl super::Story for StatusBarStory { + fn title() -> &'static str { + "StatusBar" + } + + fn description() -> &'static str { + "A horizontal bar with left/center/right regions, usually placed at the bottom." + } + + fn new_view(window: &mut Window, cx: &mut App) -> Entity { + Self::view(window, cx) + } + + fn zoomable() -> Option { + None + } +} + +impl Focusable for StatusBarStory { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for StatusBarStory { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_4() + .child( + section("Code editor").child( + v_flex().w_full().child( + StatusBar::new() + .left( + Button::new("branch").ghost().xsmall() + .icon(IconName::Github) + .label("main") + .tooltip("Git branch") + .on_click(|_, window, cx| { + window.push_notification("Switch branch", cx); + }), + ) + .left(Separator::vertical().h_3()) + .left( + h_flex() + .items_center() + .gap_2() + .child( + h_flex() + .items_center() + .gap_1() + .child( + Icon::new(IconName::CircleCheck) + .xsmall() + .text_color(cx.theme().green), + ) + .child("0"), + ) + .child( + h_flex() + .items_center() + .gap_1() + .child( + Icon::new(IconName::Info) + .xsmall() + .text_color(cx.theme().blue), + ) + .child("2"), + ), + ) + .right( + Button::new("position").ghost().xsmall() + .label("Ln 12, Col 34") + .tooltip("Go to Line/Column") + .on_click(|_, window, cx| { + window.push_notification("Go to Line/Column", cx); + }), + ) + .right(Separator::vertical().h_3()) + .right(Button::new("encoding").ghost().xsmall().label("UTF-8").on_click( + |_, window, cx| { + window.push_notification("Select encoding", cx); + }, + )) + .right(Button::new("language").ghost().xsmall().label("Rust").on_click( + |_, window, cx| { + window.push_notification("Select language", cx); + }, + )), + ), + ), + ) + .child( + section("Application").child( + v_flex().w_full().child( + StatusBar::new() + .left( + h_flex() + .items_center() + .gap_1() + .child(Icon::new(IconName::Check).xsmall()) + .child("Connected"), + ) + .child( + h_flex() + .items_center() + .gap_2() + .child(ProgressCircle::new("syncing").value(45.)) + .child("Syncing…"), + ) + .right("All changes saved") + .right( + Button::new("notifications").ghost().xsmall() + .icon(IconName::Bell) + .label("3") + .tooltip("3 notifications") + .on_click(|_, window, cx| { + window.push_notification("3 notifications", cx); + }), + ), + ), + ), + ) + // Layout cases for verifying the dynamic centering behavior. + .child( + section("Layout cases").child( + v_flex() + .w_full() + .gap_6() + .child(StatusBar::new().child("Center only → start-aligned")) + .child( + StatusBar::new() + .left("Left") + .child("Center → end (only left)"), + ) + .child( + StatusBar::new() + .child("Center → start (only right)") + .right("Right"), + ) + .child( + StatusBar::new() + .left("Left") + .child("Center → centered (left + right)") + .right("Right"), + ) + .child(StatusBar::new().left("Left").right("Right")), + ), + ) + } +} diff --git a/crates/story/src/title_bar.rs b/crates/story/src/title_bar.rs index 4777b06874..98c31c6a1a 100644 --- a/crates/story/src/title_bar.rs +++ b/crates/story/src/title_bar.rs @@ -9,7 +9,6 @@ use gpui_component::{ ActiveTheme as _, IconName, Side, Sizable as _, Theme, TitleBar, WindowExt as _, badge::Badge, button::{Button, ButtonVariants as _}, - label::Label, menu::{AppMenuBar, DropdownMenu as _}, scroll::ScrollbarShow, }; @@ -66,11 +65,6 @@ impl Render for AppTitleBar { .gap_2() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .child((self.child.clone())(window, cx)) - .child( - Label::new("theme:") - .secondary(cx.theme().theme_name()) - .text_sm(), - ) .child(self.font_size_selector.clone()) .child( Button::new("github") diff --git a/crates/ui/src/dock/dock.rs b/crates/ui/src/dock/dock.rs index 4d60a21489..449e90cf6b 100644 --- a/crates/ui/src/dock/dock.rs +++ b/crates/ui/src/dock/dock.rs @@ -357,13 +357,13 @@ impl Dock { }; match self.placement { DockPlacement::Left => { - let max_size = (area_bounds.size.width - PANEL_MIN_SIZE - right_dock_size) - .max(PANEL_MIN_SIZE); + let max_size = + (area_bounds.size.width - PANEL_MIN_SIZE - right_dock_size).max(PANEL_MIN_SIZE); self.size = size.clamp(PANEL_MIN_SIZE, max_size); } DockPlacement::Right => { - let max_size = (area_bounds.size.width - PANEL_MIN_SIZE - left_dock_size) - .max(PANEL_MIN_SIZE); + let max_size = + (area_bounds.size.width - PANEL_MIN_SIZE - left_dock_size).max(PANEL_MIN_SIZE); self.size = size.clamp(PANEL_MIN_SIZE, max_size); } DockPlacement::Bottom => { diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 47d8cf74c7..d4c2f3904f 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -66,6 +66,7 @@ pub mod sidebar; pub mod skeleton; pub mod slider; pub mod spinner; +pub mod status_bar; pub mod stepper; pub mod switch; pub mod tab; diff --git a/crates/ui/src/status_bar.rs b/crates/ui/src/status_bar.rs new file mode 100644 index 0000000000..511599f10f --- /dev/null +++ b/crates/ui/src/status_bar.rs @@ -0,0 +1,106 @@ +use gpui::{ + AnyElement, App, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window, + prelude::FluentBuilder as _, +}; +use smallvec::SmallVec; + +use crate::{ActiveTheme, StyledExt, h_flex}; + +/// A horizontal status bar, usually placed at the bottom of a window or pane. +/// +/// It is split into three regions — `left`, `center`, and `right`. This mirrors +/// the status bars found in native UI frameworks (Windows `StatusStrip`, WPF +/// `StatusBar`, macOS `NSStatusBar`): a container that holds a row of items +/// aligned to either end. +/// +/// Each region accepts any [`IntoElement`], so a string, an [`Icon`](crate::Icon), +/// a ghost `Button`, a vertical `Separator`, a custom layout, etc. can be passed +/// directly. Use a plain string for a non-interactive label. +/// +/// `left` and `right` pin items to each end. `child`/`children` add to the +/// center region, whose alignment follows the pinned ends: centered with both +/// `left` and `right`, end-aligned with only `left`, and start-aligned +/// otherwise (only `right`, or neither — like a plain container). +/// +/// ``` +/// use gpui_component::status_bar::StatusBar; +/// +/// let _ = StatusBar::new().left("Ln 1, Col 1").right("UTF-8"); +/// ``` +#[derive(IntoElement)] +pub struct StatusBar { + style: StyleRefinement, + left: SmallVec<[AnyElement; 1]>, + right: SmallVec<[AnyElement; 1]>, + children: SmallVec<[AnyElement; 1]>, +} + +impl StatusBar { + /// Create a new, empty [`StatusBar`]. + pub fn new() -> Self { + Self { + style: StyleRefinement::default(), + left: SmallVec::new(), + right: SmallVec::new(), + children: SmallVec::new(), + } + } + + /// Append an element to the left region. Call multiple times to add more. + pub fn left(mut self, child: impl IntoElement) -> Self { + self.left.push(child.into_any_element()); + self + } + + /// Append an element to the right region. Call multiple times to add more. + pub fn right(mut self, child: impl IntoElement) -> Self { + self.right.push(child.into_any_element()); + self + } +} + +/// `child` / `children` add to the center region, so a `StatusBar` without +/// `left`/`right` items behaves like a plain container. +impl ParentElement for StatusBar { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); + } +} + +impl Styled for StatusBar { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl RenderOnce for StatusBar { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + // The center aligns by which ends are pinned: centered with both left + // and right, end-aligned with only left, otherwise start-aligned (only + // right, or neither) — so a bar with just `child`s reads like a container. + let has_left = !self.left.is_empty(); + let has_right = !self.right.is_empty(); + let region = || h_flex().overflow_hidden().items_center().gap_2(); + + h_flex() + .items_center() + .gap_2() + .py_1() + .px_2() + .border_t_1() + .border_color(cx.theme().status_bar_border) + .bg(cx.theme().status_bar) + .text_xs() + .text_color(cx.theme().muted_foreground) + .refine_style(&self.style) + .when(has_left, |this| this.child(region().children(self.left))) + .child( + region() + .flex_1() + .when(has_left && has_right, |this| this.justify_center()) + .when(has_left && !has_right, |this| this.justify_end()) + .children(self.children), + ) + .when(has_right, |this| this.child(region().children(self.right))) + } +} diff --git a/crates/ui/src/theme/default-theme.json b/crates/ui/src/theme/default-theme.json index 2e19e66f70..d6c433535e 100644 --- a/crates/ui/src/theme/default-theme.json +++ b/crates/ui/src/theme/default-theme.json @@ -91,6 +91,8 @@ "tiles.background": "#fafafa", "title_bar.background": "#F8F8F8", "title_bar.border": "#e5e5e5", + "status_bar.background": "#F8F8F8", + "status_bar.border": "#e5e5e5", "warning.background": "yellow-500", "warning.foreground": "neutral-50", "overlay": "#0000000d", @@ -290,6 +292,8 @@ "tiles.background": "#171717", "title_bar.background": "#171717", "title_bar.border": "#262626", + "status_bar.background": "#171717", + "status_bar.border": "#262626", "warning.background": "yellow-400", "warning.foreground": "yellow-600", "overlay": "#00000033", diff --git a/crates/ui/src/theme/schema.rs b/crates/ui/src/theme/schema.rs index c936be8740..e46e26a55d 100644 --- a/crates/ui/src/theme/schema.rs +++ b/crates/ui/src/theme/schema.rs @@ -359,6 +359,12 @@ pub struct ThemeConfigColors { /// TitleBar border color. #[serde(rename = "title_bar.border")] pub title_bar_border: Option, + /// StatusBar background color, use for the bottom status bar. + #[serde(rename = "status_bar.background")] + pub status_bar: Option, + /// StatusBar border color. + #[serde(rename = "status_bar.border")] + pub status_bar_border: Option, /// Background color for Tiles. #[serde(rename = "tiles.background")] pub tiles: Option, @@ -651,6 +657,8 @@ impl ThemeColor { apply_color!(table_row_border, fallback = self.border); apply_color!(title_bar, fallback = self.background); apply_color!(title_bar_border, fallback = self.border); + apply_color!(status_bar, fallback = self.title_bar); + apply_color!(status_bar_border, fallback = self.title_bar_border); apply_color!(tiles, fallback = self.background); apply_color!(overlay); apply_color!(window_border, fallback = self.border); diff --git a/crates/ui/src/theme/theme_color.rs b/crates/ui/src/theme/theme_color.rs index 9f7c1b79db..da241645d4 100644 --- a/crates/ui/src/theme/theme_color.rs +++ b/crates/ui/src/theme/theme_color.rs @@ -199,6 +199,10 @@ pub struct ThemeColor { pub title_bar: Hsla, /// TitleBar border color. pub title_bar_border: Hsla, + /// StatusBar background color, use for the bottom status bar. + pub status_bar: Hsla, + /// StatusBar border color. + pub status_bar_border: Hsla, /// Background color for Tiles. pub tiles: Hsla, /// Warning background color. diff --git a/docs/docs/components/index.md b/docs/docs/components/index.md index c051003d06..e0ec724ff8 100644 --- a/docs/docs/components/index.md +++ b/docs/docs/components/index.md @@ -56,6 +56,7 @@ collapsed: false - [Scrollable](scrollable) - Scrollable containers - [Sheet](sheet) - Slide-in panel from edges - [Sidebar](sidebar) - Navigation sidebar +- [StatusBar](status-bar) - Bottom status bar with left/center/right regions ### Advanced Components diff --git a/docs/docs/components/status-bar.md b/docs/docs/components/status-bar.md new file mode 100644 index 0000000000..929e1a6a47 --- /dev/null +++ b/docs/docs/components/status-bar.md @@ -0,0 +1,101 @@ +--- +title: StatusBar +description: A horizontal status bar with left, center, and right regions, usually placed at the bottom of a window or pane. +--- + +# StatusBar + +StatusBar is a horizontal bar split into three regions — `left`, `center`, and `right`. It is usually placed at the bottom of a window or pane to show contextual information and quick actions. + +The design mirrors the status bars found in native UI frameworks: Windows `StatusStrip`, WPF `StatusBar`, and macOS `NSStatusBar`. + +## Import + +```rust +use gpui_component::status_bar::StatusBar; +``` + +## Regions + +Pass any `impl IntoElement` — a string, an `Icon`, a `Button`, a custom layout, etc. — to a region. `left` and `right` pin items to each end; `child` / `children` add to the center, whose alignment follows the pinned ends — centered with both `left` and `right`, end-aligned with only `left`, start-aligned otherwise (only `right`, or neither, like a plain container). Call a method multiple times to add more. + +- For a **non-interactive label**, pass a plain string — it inherits the bar's text style and has no hover. +- For a **clickable button**, pass a ghost, xsmall `Button` — `Button::new(id).ghost().xsmall()` — so buttons stay a consistent size. Chain `label`, `icon`, `tooltip`, `on_click`, etc. +- For a **separator**, pass `Separator::vertical()`. +- For anything else, pass the element directly. + +## Usage + +### Labels + +```rust +StatusBar::new() + .left("Ready") + .child("README.md") + .right("UTF-8") +``` + +### Buttons + +```rust +StatusBar::new() + .left( + Button::new("branch").ghost().xsmall() + .icon(IconName::Github) + .label("main") + .on_click(|_, window, cx| { /* ... */ }), + ) + .right( + Button::new("go-to-line").ghost().xsmall() + .label("Ln 1, Col 1") + .tooltip("Go to Line/Column") + .on_click(cx.listener(|this, _, window, cx| { /* ... */ })), + ) +``` + +### Separators and custom elements + +```rust +StatusBar::new() + .left(Button::new("branch").ghost().xsmall().icon(IconName::Github).label("main")) + .left(Separator::vertical()) + .left( + // Any custom element works. + h_flex() + .items_center() + .gap_1() + .child(Icon::new(IconName::CircleCheck).xsmall()) + .child("0 problems"), + ) + .child(Progress::new("indexing").value(60.).w_24()) +``` + +### Custom styling + +`StatusBar` implements `Styled`, so any style method overrides the defaults. + +```rust +StatusBar::new() + .bg(cx.theme().secondary) + .border_color(cx.theme().border) + .left("Ready") +``` + +## API Reference + +### StatusBar + +| Method | Description | +| ----------------- | ---------------------------------------------------- | +| `new()` | Create a new, empty status bar | +| `left(child)` | Append an element to the left region (call to add more) | +| `right(child)` | Append an element to the right region | +| `child(c)` / `children(cs)` | Add element(s) to the center region | + +Each region method takes `impl IntoElement`. `StatusBar` also implements `Styled`, so style methods (`bg`, `border_color`, `py`, etc.) can override the defaults. + +## Notes + +- The center (via `child` / `children`) is centered with both `left` and `right`, end-aligned with only `left`, and start-aligned otherwise (only `right`, or neither — like a plain container). +- Use a plain string (or any non-interactive element) for read-only items to avoid the button hover effect; use a ghost xsmall `Button` only for clickable items. +- Colors come from the `status_bar` (background) and `status_bar_border` theme tokens, which fall back to `background` / `border`. diff --git a/docs/zh-CN/docs/components/index.md b/docs/zh-CN/docs/components/index.md index 64776bbea9..f309407858 100644 --- a/docs/zh-CN/docs/components/index.md +++ b/docs/zh-CN/docs/components/index.md @@ -37,6 +37,7 @@ collapsed: false - [Resizable](resizable) - 可调整大小的面板 - [Scrollable](scrollable) - 可滚动容器 - [Sidebar](sidebar) - 侧边栏导航 +- [StatusBar](status-bar) - 底部状态栏,含左/中/右三区 - [Chart](chart) - 图表组件 - [DataTable](data-table) - 高性能数据表格 - [Tree](tree) - 树形结构组件 diff --git a/docs/zh-CN/docs/components/status-bar.md b/docs/zh-CN/docs/components/status-bar.md new file mode 100644 index 0000000000..67ec0b9601 --- /dev/null +++ b/docs/zh-CN/docs/components/status-bar.md @@ -0,0 +1,101 @@ +--- +title: StatusBar +description: 一个分为左、中、右三个区域的水平状态栏,通常放置在窗口或面板底部。 +--- + +# StatusBar + +StatusBar 是一个水平栏,分为 `left`、`center`、`right` 三个区域。它通常放置在窗口或面板底部,用于显示上下文信息和快捷操作。 + +其设计参考了原生 UI 框架中的状态栏:Windows 的 `StatusStrip`、WPF 的 `StatusBar` 以及 macOS 的 `NSStatusBar`。 + +## 引入 + +```rust +use gpui_component::status_bar::StatusBar; +``` + +## 区域 + +向区域传入任意 `impl IntoElement` —— 字符串、`Icon`、`Button`、自定义布局等。`left` 和 `right` 把项固定在两端;`child` / `children` 添加到中间区域,其对齐方式取决于固定了哪一端 —— 同时有 `left` 和 `right` 时居中,只有 `left` 时右对齐,否则左对齐(只有 `right`,或两者都没有,像普通容器一样)。多次调用即可追加更多。 + +- **不可交互的标签**:直接传字符串 —— 它会继承状态栏的文字样式,且没有 hover。 +- **可点击的按钮**:传入一个 ghost、xsmall 的 `Button` —— `Button::new(id).ghost().xsmall()` —— 保证按钮尺寸一致。可链式调用 `label`、`icon`、`tooltip`、`on_click` 等。 +- **分隔线**:传入 `Separator::vertical()`。 +- **其他任意内容**:直接传该元素。 + +## 用法 + +### 标签 + +```rust +StatusBar::new() + .left("Ready") + .child("README.md") + .right("UTF-8") +``` + +### 按钮 + +```rust +StatusBar::new() + .left( + Button::new("branch").ghost().xsmall() + .icon(IconName::Github) + .label("main") + .on_click(|_, window, cx| { /* ... */ }), + ) + .right( + Button::new("go-to-line").ghost().xsmall() + .label("Ln 1, Col 1") + .tooltip("Go to Line/Column") + .on_click(cx.listener(|this, _, window, cx| { /* ... */ })), + ) +``` + +### 分割线与自定义元素 + +```rust +StatusBar::new() + .left(Button::new("branch").ghost().xsmall().icon(IconName::Github).label("main")) + .left(Separator::vertical()) + .left( + // 任意自定义元素都可以。 + h_flex() + .items_center() + .gap_1() + .child(Icon::new(IconName::CircleCheck).xsmall()) + .child("0 problems"), + ) + .child(Progress::new("indexing").value(60.).w_24()) +``` + +### 自定义样式 + +`StatusBar` 实现了 `Styled`,因此任意样式方法都会覆盖默认值。 + +```rust +StatusBar::new() + .bg(cx.theme().secondary) + .border_color(cx.theme().border) + .left("Ready") +``` + +## API 参考 + +### StatusBar + +| 方法 | 说明 | +| ---------------- | ------------------------------------------ | +| `new()` | 创建一个空的状态栏 | +| `left(child)` | 向左侧区域追加一个元素(可多次调用) | +| `right(child)` | 向右侧区域追加一个元素 | +| `child(c)` / `children(cs)` | 向中间区域添加元素 | + +每个区域方法接受 `impl IntoElement`。`StatusBar` 同时实现了 `Styled`,样式方法(`bg`、`border_color`、`py` 等)可以覆盖默认值。 + +## 注意事项 + +- 中间区域(通过 `child` / `children`)在同时有 `left` 和 `right` 时居中,只有 `left` 时右对齐,否则左对齐(只有 `right`,或两者都没有 —— 像普通容器一样)。 +- 只读项请用纯字符串(或任意不可交互元素),以避免按钮的 hover 效果;只有可点击项才用 ghost、xsmall 的 `Button`。 +- 颜色取自 `status_bar`(背景)和 `status_bar_border`(边框)主题变量,缺省回退到 `background` / `border`。 diff --git a/themes/everforest.json b/themes/everforest.json index 56229dc182..a5105e4d2f 100644 --- a/themes/everforest.json +++ b/themes/everforest.json @@ -40,6 +40,7 @@ "tab_bar.background": "#F4F1E2", "title_bar.background": "#F9F5E4", "title_bar.border": "#E7E5D4", + "status_bar.background": "#F9F5E4", "base.red": "#e67e80", "base.green": "#a7c080", "base.yellow": "#dbbc7f", diff --git a/themes/hybrid.json b/themes/hybrid.json index dd261f8d16..133d5547f4 100644 --- a/themes/hybrid.json +++ b/themes/hybrid.json @@ -41,6 +41,7 @@ "tab_bar.background": "#e8e8e8", "title_bar.background": "#D0D0D0", "title_bar.border": "#B3B3B3", + "status_bar.background": "#DADADA", "base.red": "#5F0000", "base.green": "#005F00", "base.yellow": "#948000", diff --git a/themes/macos-classic.json b/themes/macos-classic.json index 70c2ac87bc..9af13b9a52 100644 --- a/themes/macos-classic.json +++ b/themes/macos-classic.json @@ -38,6 +38,7 @@ "tab_bar.background": "#E9E9E9", "title_bar.background": "#FEFEFE", "title_bar.border": "#DADADA", + "status_bar.background": "#E9E9E9", "base.yellow": "#B59A00", "base.red": "#d21f07", "base.blue": "#0060de", diff --git a/themes/mellifluous.json b/themes/mellifluous.json index fac6457425..afcfad917d 100644 --- a/themes/mellifluous.json +++ b/themes/mellifluous.json @@ -45,6 +45,7 @@ "tab_bar.background": "#E7E7E7", "title_bar.background": "#dfdfdf", "title_bar.border": "#CACACA", + "status_bar.background": "#dfdfdf", "base.red": "#C95954", "base.green": "#828040", "base.yellow": "#c98f54",