use std::rc::Rc; use gpui::{ App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, }; use crate::{ motion::Spinner, primitives::{Icon, IconName}, shell::StatusBarItem, theme::{ActiveTheme, IconSize, TextSize}, }; type OnPress = Rc; fn item( id: ElementId, label: impl Into, tooltip: &'static str, press: Option, ) -> StatusBarItem { let item = StatusBarItem::new(id).label(label).tooltip(tooltip); match press { Some(press) => item.on_click(move |_, window, cx| press(window, cx)), None => item, } } macro_rules! pressable { ($name:ident) => { impl $name { pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { self.press = Some(Rc::new(handler)); self } } }; } /// The cursor's line and column, and how many characters are selected when some are. #[derive(IntoElement)] pub struct CursorPosition { id: ElementId, line: usize, column: usize, selected: usize, press: Option, } impl CursorPosition { /// `line` and `column` count from one. pub fn new(id: impl Into, line: usize, column: usize) -> Self { assert!(line > 0 && column > 0, "lines and columns count from one"); Self { id: id.into(), line, column, selected: 0, press: None, } } pub fn selected(mut self, characters: usize) -> Self { self.selected = characters; self } } pressable!(CursorPosition); impl RenderOnce for CursorPosition { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let mut label = format!("Ln {}, Col {}", self.line, self.column); if self.selected > 0 { label.push_str(&format!(" ({} selected)", self.selected)); } item(self.id, label, "Go to line", self.press) } } /// The language a file is read as. #[derive(IntoElement)] pub struct LanguageMode { id: ElementId, language: SharedString, press: Option, } impl LanguageMode { pub fn new(id: impl Into, language: impl Into) -> Self { Self { id: id.into(), language: language.into(), press: None, } } } pressable!(LanguageMode); impl RenderOnce for LanguageMode { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { item( self.id, self.language, "Select the language mode", self.press, ) } } /// The text encoding a file is saved in. #[derive(IntoElement)] pub struct Encoding { id: ElementId, name: SharedString, press: Option, } impl Encoding { pub fn new(id: impl Into, name: impl Into) -> Self { Self { id: id.into(), name: name.into(), press: None, } } } pressable!(Encoding); impl RenderOnce for Encoding { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { item( self.id, self.name, "Reopen or save with an encoding", self.press, ) } } /// How a file ends its lines. #[derive(IntoElement)] pub struct LineEnding { id: ElementId, crlf: bool, press: Option, } impl LineEnding { /// `crlf` for carriage return and line feed, else line feed alone. pub fn new(id: impl Into, crlf: bool) -> Self { Self { id: id.into(), crlf, press: None, } } } pressable!(LineEnding); impl RenderOnce for LineEnding { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { item( self.id, if self.crlf { "CRLF" } else { "LF" }, "Change the line ending", self.press, ) } } /// How a file indents: spaces or tabs, and how wide. #[derive(IntoElement)] pub struct IndentSettings { id: ElementId, spaces: bool, size: usize, press: Option, } impl IndentSettings { pub fn new(id: impl Into, spaces: bool, size: usize) -> Self { assert!(size > 0, "an indent is at least one column"); Self { id: id.into(), spaces, size, press: None, } } } pressable!(IndentSettings); impl RenderOnce for IndentSettings { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let label = if self.spaces { format!("Spaces: {}", self.size) } else { format!("Tab Size: {}", self.size) }; item(self.id, label, "Change the indentation", self.press) } } /// The branch checked out, a mark when the tree has changes, and how far it is ahead of and behind its upstream. #[derive(IntoElement)] pub struct BranchIndicator { id: ElementId, branch: SharedString, dirty: bool, ahead: usize, behind: usize, press: Option, } impl BranchIndicator { pub fn new(id: impl Into, branch: impl Into) -> Self { Self { id: id.into(), branch: branch.into(), dirty: false, ahead: 0, behind: 0, press: None, } } pub fn dirty(mut self, dirty: bool) -> Self { self.dirty = dirty; self } /// Commits not yet pushed, and commits not yet pulled. pub fn sync(mut self, ahead: usize, behind: usize) -> Self { self.ahead = ahead; self.behind = behind; self } } pressable!(BranchIndicator); impl RenderOnce for BranchIndicator { fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { let mut label = format!("{}{}", self.branch, if self.dirty { "*" } else { "" }); if self.ahead > 0 || self.behind > 0 { label.push_str(&format!(" ↑{} ↓{}", self.ahead, self.behind)); } item(self.id, label, "Checkout a branch", self.press).icon(IconName::GitBranch) } } /// Where a language server stands. #[derive(Clone, Debug, PartialEq, Eq)] pub enum LspState { Starting, Ready, /// Working on something it names. Busy(SharedString), Failed(SharedString), } /// A language server's name and state: a spinner while it starts or works, a check when ready, a mark when it failed. #[derive(IntoElement)] pub struct LspStatus { id: ElementId, server: SharedString, state: LspState, press: Option, } impl LspStatus { pub fn new(id: impl Into, server: impl Into, state: LspState) -> Self { Self { id: id.into(), server: server.into(), state, press: None, } } } pressable!(LspStatus); impl RenderOnce for LspStatus { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let colors = cx.theme().colors.clone(); let (lead, label, tooltip) = match &self.state { LspState::Starting => ( Spinner::new((self.id.clone(), "spin")) .size(IconSize::Xs) .into_any_element(), format!("{} starting", self.server), "Starting", ), LspState::Busy(task) => ( Spinner::new((self.id.clone(), "spin")) .size(IconSize::Xs) .into_any_element(), format!("{} · {task}", self.server), "Working", ), LspState::Ready => ( Icon::new(IconName::CircleCheck) .size(IconSize::Xs) .color(colors.fg_muted) .into_any_element(), self.server.to_string(), "Ready", ), LspState::Failed(why) => ( Icon::new(IconName::CircleAlert) .size(IconSize::Xs) .color(colors.danger) .into_any_element(), format!("{} · {why}", self.server), "Failed", ), }; item(self.id, label, tooltip, self.press).leading(lead) } } /// A bell with how many notifications wait, quiet when there are none. #[derive(IntoElement)] pub struct NotificationBell { id: ElementId, count: usize, press: Option, } impl NotificationBell { pub fn new(id: impl Into, count: usize) -> Self { Self { id: id.into(), count, press: None, } } } pressable!(NotificationBell); impl RenderOnce for NotificationBell { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors.clone(); let bell = div() .relative() .child( Icon::new(IconName::Bell) .size(IconSize::Xs) .color(colors.fg_muted), ) .children((self.count > 0).then(|| { div() .absolute() .top_neg_1() .right_neg_1p5() .px_0p5() .rounded_full() .bg(colors.accent) .text_color(colors.on_accent) .text_size(theme.text_size(TextSize::Xs) * 0.75) .line_height(theme.text_size(TextSize::Xs) * 0.9) .child(self.count.min(99).to_string()) })); let tooltip = if self.count == 0 { "No new notifications" } else { "Notifications" }; let item = StatusBarItem::new(self.id).tooltip(tooltip).leading(bell); match self.press { Some(press) => item.on_click(move |_, window, cx| press(window, cx)), None => item, } } }