diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg new file mode 100644 index 0000000000..8b755e8063 --- /dev/null +++ b/assets/icons/copy.svg @@ -0,0 +1 @@ + diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 0a0f4da893..dd01f90b9f 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -774,24 +774,39 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { Arc::new(move |_| { h_stack() .id("diagnostic header") - .gap_3() - .bg(gpui::red()) - .map(|stack| { - let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { - IconElement::new(Icon::XCircle).color(Color::Error) - } else { - IconElement::new(Icon::ExclamationTriangle).color(Color::Warning) - }; - - stack.child(div().pl_8().child(icon)) - }) - .when_some(diagnostic.source.as_ref(), |stack, source| { - stack.child(Label::new(format!("{source}:")).color(Color::Accent)) - }) - .child(HighlightedLabel::new(message.clone(), highlights.clone())) - .when_some(diagnostic.code.as_ref(), |stack, code| { - stack.child(Label::new(code.clone())) - }) + .py_2() + .pl_10() + .pr_5() + .w_full() + .justify_between() + .gap_2() + .child( + h_stack() + .gap_3() + .map(|stack| { + let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { + IconElement::new(Icon::XCircle).color(Color::Error) + } else { + IconElement::new(Icon::ExclamationTriangle).color(Color::Warning) + }; + stack.child(icon) + }) + .child( + h_stack() + .gap_1() + .child(HighlightedLabel::new(message.clone(), highlights.clone())) + .when_some(diagnostic.code.as_ref(), |stack, code| { + stack.child(Label::new(format!("({code})")).color(Color::Muted)) + }), + ), + ) + .child( + h_stack() + .gap_1() + .when_some(diagnostic.source.as_ref(), |stack, source| { + stack.child(Label::new(format!("{source}")).color(Color::Muted)) + }), + ) .into_any_element() }) } @@ -802,11 +817,22 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { label.into_any_element() } else { h_stack() - .bg(gpui::red()) - .child(IconElement::new(Icon::XCircle)) - .child(Label::new(summary.error_count.to_string())) - .child(IconElement::new(Icon::ExclamationTriangle)) - .child(Label::new(summary.warning_count.to_string())) + .gap_1() + .when(summary.error_count > 0, |then| { + then.child( + h_stack() + .gap_1() + .child(IconElement::new(Icon::XCircle).color(Color::Error)) + .child(Label::new(summary.error_count.to_string())), + ) + }) + .when(summary.warning_count > 0, |then| { + then.child( + h_stack() + .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)) + .child(Label::new(summary.warning_count.to_string())), + ) + }) .into_any_element() } } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 92fa4ca792..e9979a3ae2 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -100,8 +100,10 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::prelude::*; -use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip}; +use ui::{ + h_stack, v_stack, ButtonSize, ButtonStyle, HighlightedLabel, Icon, IconButton, Popover, Tooltip, +}; +use ui::{prelude::*, IconSize}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, @@ -9689,20 +9691,44 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend let message = diagnostic.message; Arc::new(move |cx: &mut BlockContext| { let message = message.clone(); + let copy_id: SharedString = format!("copy-{}", cx.block_id.clone()).to_string().into(); + // TODO: `cx.write_to_clipboard` is not implemented in tests. + // let write_to_clipboard = cx.write_to_clipboard(ClipboardItem::new(message.clone())); + + // TODO: Nate: We should tint the background of the block with the severity color + // We need to extend the theme before we can do this v_stack() .id(cx.block_id) + .relative() .size_full() .bg(gpui::red()) .children(highlighted_lines.iter().map(|(line, highlights)| { - div() + let group_id = cx.block_id.to_string(); + + h_stack() + .group(group_id.clone()) + .gap_2() + .absolute() + .left(cx.anchor_x) + .px_1p5() .child(HighlightedLabel::new(line.clone(), highlights.clone())) - .ml(cx.anchor_x) + .child( + div() + .border() + .border_color(gpui::red()) + .invisible() + .group_hover(group_id, |style| style.visible()) + .child( + IconButton::new(copy_id.clone(), Icon::Copy) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .style(ButtonStyle::Transparent) + // TODO: `cx.write_to_clipboard` is not implemented in tests. + // .on_click(cx.listener(move |_, _, cx| write_to_clipboard)) + .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)), + ), + ) })) - .cursor_pointer() - .on_click(cx.listener(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new(message.clone())); - })) - .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)) .into_any_element() }) } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3abe5a37f9..824b8c7df8 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -51,8 +51,10 @@ use std::{ }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; -use ui::prelude::*; -use ui::{h_stack, IconButton, Tooltip}; +use ui::{ + h_stack, ButtonLike, ButtonStyle, Disclosure, IconButton, IconElement, IconSize, Label, Tooltip, +}; +use ui::{prelude::*, Icon}; use util::ResultExt; use workspace::item::Item; @@ -2223,7 +2225,8 @@ impl EditorElement { .as_ref() .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) .unwrap_or_default(); - let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { + + let jump_handler = project::File::from_dyn(buffer.file()).map(|file| { let jump_path = ProjectPath { worktree_id: file.worktree_id(cx), path: file.path.clone(), @@ -2234,11 +2237,11 @@ impl EditorElement { .map_or(range.context.start, |primary| primary.start); let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - IconButton::new(block_id, ui::Icon::ArrowUpRight) - .on_click(cx.listener_for(&self.editor, move |editor, e, cx| { - editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); - })) - .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)) + let jump_handler = cx.listener_for(&self.editor, move |editor, e, cx| { + editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); + }); + + jump_handler }); let element = if *starts_new_buffer { @@ -2253,25 +2256,108 @@ impl EditorElement { .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); } - h_stack() - .id("path header block") - .size_full() - .bg(gpui::red()) - .child( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .children(parent_path) - .children(jump_icon) // .p_x(gutter_padding) + let is_open = true; + + div().id("path header container").size_full().p_1p5().child( + h_stack() + .id("path header block") + .py_1p5() + .pl_3() + .pr_2() + .rounded_lg() + .shadow_md() + .border() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_subheader_background) + .justify_between() + .cursor_pointer() + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .on_click(cx.listener(|_editor, _event, _cx| { + // TODO: Implement collapsing path headers + todo!("Clicking path header") + })) + .child( + h_stack() + .gap_3() + // TODO: Add open/close state and toggle action + .child( + div().border().border_color(gpui::red()).child( + ButtonLike::new("path-header-disclosure-control") + .style(ButtonStyle::Subtle) + .child(IconElement::new(match is_open { + true => Icon::ChevronDown, + false => Icon::ChevronRight, + })), + ), + ) + .child( + h_stack() + .gap_2() + .child(Label::new( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + )) + .when_some(parent_path, |then, path| { + then.child(Label::new(path).color(Color::Muted)) + }), + ), + ) + .children(jump_handler.map(|jump_handler| { + IconButton::new(block_id, Icon::ArrowUpRight) + .style(ButtonStyle::Subtle) + .on_click(jump_handler) + .tooltip(|cx| { + Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx) + }) + })), // .p_x(gutter_padding) + ) } else { let text_style = style.text.clone(); h_stack() .id("collapsed context") .size_full() - .bg(gpui::red()) - .child("⋯") - .children(jump_icon) // .p_x(gutter_padding) + .gap(gutter_padding) + .child( + h_stack() + .justify_end() + .flex_none() + .w(gutter_width - gutter_padding) + .h_full() + .text_buffer(cx) + .text_color(cx.theme().colors().editor_line_number) + .child("..."), + ) + .map(|this| { + if let Some(jump_handler) = jump_handler { + this.child( + ButtonLike::new("jump to collapsed context") + .style(ButtonStyle::Transparent) + .full_width() + .on_click(jump_handler) + .tooltip(|cx| { + Tooltip::for_action( + "Jump to Buffer", + &OpenExcerpts, + cx, + ) + }) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().border_variant) + .group_hover("", |style| { + style.bg(cx.theme().colors().border) + }), + ), + ) + } else { + this.child(div().size_full().bg(gpui::green())) + } + }) + // .child("⋯") + // .children(jump_icon) // .p_x(gutter_padding) }; element.into_any() } diff --git a/crates/ui2/src/components/button/button.rs b/crates/ui2/src/components/button/button.rs index d5f8ce9287..c1262321ce 100644 --- a/crates/ui2/src/components/button/button.rs +++ b/crates/ui2/src/components/button/button.rs @@ -1,4 +1,4 @@ -use gpui::AnyView; +use gpui::{AnyView, DefiniteLength}; use crate::prelude::*; use crate::{ @@ -88,6 +88,18 @@ impl Clickable for Button { } } +impl FixedWidth for Button { + fn width(mut self, width: DefiniteLength) -> Self { + self.base = self.base.width(width); + self + } + + fn full_width(mut self) -> Self { + self.base = self.base.full_width(); + self + } +} + impl ButtonCommon for Button { fn id(&self) -> &ElementId { self.base.id() diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 71aa31ced2..4bef6bff77 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -1,3 +1,4 @@ +use gpui::{relative, DefiniteLength}; use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful}; use smallvec::SmallVec; @@ -246,6 +247,7 @@ pub struct ButtonLike { pub(super) style: ButtonStyle, pub(super) disabled: bool, pub(super) selected: bool, + pub(super) width: Option, size: ButtonSize, tooltip: Option AnyView>>, on_click: Option>, @@ -259,6 +261,7 @@ impl ButtonLike { style: ButtonStyle::default(), disabled: false, selected: false, + width: None, size: ButtonSize::Default, tooltip: None, children: SmallVec::new(), @@ -288,6 +291,18 @@ impl Clickable for ButtonLike { } } +impl FixedWidth for ButtonLike { + fn width(mut self, width: DefiniteLength) -> Self { + self.width = Some(width); + self + } + + fn full_width(mut self) -> Self { + self.width = Some(relative(1.)); + self + } +} + impl ButtonCommon for ButtonLike { fn id(&self) -> &ElementId { &self.id @@ -321,7 +336,10 @@ impl RenderOnce for ButtonLike { fn render(self, cx: &mut WindowContext) -> Self::Rendered { h_stack() .id(self.id.clone()) + .group("") + .flex_none() .h(self.size.height()) + .when_some(self.width, |this, width| this.w(width)) .rounded_md() .gap_1() .px_1() diff --git a/crates/ui2/src/components/button/icon_button.rs b/crates/ui2/src/components/button/icon_button.rs index 981eb3aaca..94431ef642 100644 --- a/crates/ui2/src/components/button/icon_button.rs +++ b/crates/ui2/src/components/button/icon_button.rs @@ -1,4 +1,4 @@ -use gpui::{Action, AnyView}; +use gpui::{Action, AnyView, DefiniteLength}; use crate::prelude::*; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize}; @@ -69,6 +69,18 @@ impl Clickable for IconButton { } } +impl FixedWidth for IconButton { + fn width(mut self, width: DefiniteLength) -> Self { + self.base = self.base.width(width); + self + } + + fn full_width(mut self) -> Self { + self.base = self.base.full_width(); + self + } +} + impl ButtonCommon for IconButton { fn id(&self) -> &ElementId { self.base.id() diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 05dac731dd..a993a54e15 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -27,6 +27,7 @@ pub enum Icon { Bolt, CaseSensitive, Check, + Copy, ChevronDown, ChevronLeft, ChevronRight, @@ -100,6 +101,7 @@ impl Icon { Icon::Bolt => "icons/bolt.svg", Icon::CaseSensitive => "icons/case_insensitive.svg", Icon::Check => "icons/check.svg", + Icon::Copy => "icons/copy.svg", Icon::ChevronDown => "icons/chevron_down.svg", Icon::ChevronLeft => "icons/chevron_left.svg", Icon::ChevronRight => "icons/chevron_right.svg", diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index cb224fd0fe..e567830d6c 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,4 +1,6 @@ use gpui::{px, Styled, WindowContext}; +use settings::Settings; +use theme::ThemeSettings; use crate::prelude::*; use crate::{ElevationIndex, UITextSize}; @@ -60,6 +62,18 @@ pub trait StyledExt: Styled + Sized { self.text_size(size) } + /// The font size for buffer text. + /// + /// Retrieves the default font size, or the user's custom font size if set. + /// + /// This should only be used for text that is displayed in a buffer, + /// or other places that text needs to match the user's buffer font size. + fn text_buffer(self, cx: &mut WindowContext) -> Self { + let settings = ThemeSettings::get_global(cx); + + self.text_size(settings.buffer_font_size) + } + /// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements /// /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`